From dc2276097852d2081e1890bdaf17a5414ce39fe5 Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Tue, 8 Feb 2011 14:42:33 +0000 Subject: [PATCH] Introduce FeatureSpecification support Introduce FeatureSpecification interface and implementations FeatureSpecification objects decouple the configuration of spring container features from the concern of parsing XML namespaces, allowing for reuse in code-based configuration (see @Feature* annotations below). * ComponentScanSpec * TxAnnotationDriven * MvcAnnotationDriven * MvcDefaultServletHandler * MvcResources * MvcViewControllers Refactor associated BeanDefinitionParsers to delegate to new impls above The following BeanDefinitionParser implementations now deal only with the concern of XML parsing. Validation is handled by their corresponding FeatureSpecification object. Bean definition creation and registration is handled by their corresponding FeatureSpecificationExecutor type. * ComponentScanBeanDefinitionParser * AnnotationDrivenBeanDefinitionParser (tx) * AnnotationDrivenBeanDefinitionParser (mvc) * DefaultServletHandlerBeanDefinitionParser * ResourcesBeanDefinitionParser * ViewControllerBeanDefinitionParser Update AopNamespaceUtils to decouple from XML (DOM API) Methods necessary for executing TxAnnotationDriven specification (and eventually, the AspectJAutoProxy specification) have been added that accept boolean arguments for whether to proxy target classes and whether to expose the proxy via threadlocal. Methods that accepted and introspected DOM Element objects still exist but have been deprecated. Introduce @FeatureConfiguration classes and @Feature methods Allow for creation and configuration of FeatureSpecification objects at the user level. A companion for @Configuration classes allowing for completely code-driven configuration of the Spring container. See changes in ConfigurationClassPostProcessor for implementation details. See Feature*Tests for usage examples. FeatureTestSuite in .integration-tests is a JUnit test suite designed to aggregate all BDP and Feature* related tests for a convenient way to confirm that Feature-related changes don't break anything. Uncomment this test and execute from Eclipse / IDEA. Due to classpath issues, this cannot be compiled by Ant/Ivy at the command line. Introduce @FeatureAnnotation meta-annotation and @ComponentScan impl @FeatureAnnotation provides an alternate mechanism for creating and executing FeatureSpecification objects. See @ComponentScan and its corresponding ComponentScanAnnotationParser implementation for details. See ComponentScanAnnotationIntegrationTests for usage examples Introduce Default[Formatting]ConversionService implementations Allows for convenient instantiation of ConversionService objects containing defaults appropriate for most environments. Replaces similar support originally in ConversionServiceFactory (which is now deprecated). This change was justified by the need to avoid use of FactoryBeans in @Configuration classes (such as FormattingConversionServiceFactoryBean). It is strongly preferred that users simply instantiate and configure the objects that underlie our FactoryBeans. In the case of the ConversionService types, the easiest way to do this is to create Default* subtypes. This also follows convention with the rest of the framework. Minor updates to util classes All in service of changes above. See diffs for self-explanatory details. * BeanUtils * ObjectUtils * ReflectionUtils git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@3954 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../aop/config/AopNamespaceUtils.java | 42 +- .../org/springframework/beans/BeanUtils.java | 23 +- .../parsing/BeanDefinitionRegistrar.java | 25 + .../factory/parsing/ComponentRegistrar.java | 24 + .../beans/factory/parsing/ReaderContext.java | 5 +- .../parsing/SimpleProblemCollector.java | 59 +++ .../support/BeanDefinitionBuilder.java | 2 +- .../support/BeanDefinitionReaderUtils.java | 2 +- .../factory/xml/BeanDefinitionParser.java | 17 +- .../beans/factory/xml/ParserContext.java | 7 +- .../beans/factory/xml/XmlReaderContext.java | 5 + .../DefaultListableBeanFactoryTests.java | 31 +- org.springframework.context/.springBeans | 1 + .../AnnotatedBeanDefinitionReader.java | 3 +- .../AnnotationScopeMetadataResolver.java | 7 +- .../annotation/BeanAnnotationHelper.java | 51 ++ .../ClassPathBeanDefinitionScanner.java | 5 +- ...athScanningCandidateComponentProvider.java | 3 +- .../context/annotation/ComponentScan.java | 118 ++++- .../ComponentScanAnnotationParser.java | 97 ++++ .../ComponentScanBeanDefinitionParser.java | 276 ++--------- .../annotation/ComponentScanExecutor.java | 110 +++++ .../context/annotation/ComponentScanSpec.java | 460 ++++++++++++++++++ .../context/annotation/Configuration.java | 2 +- .../annotation/ConfigurationClass.java | 3 +- ...onfigurationClassBeanDefinitionReader.java | 82 +++- .../ConfigurationClassEnhancer.java | 25 +- .../annotation/ConfigurationClassMethod.java | 4 +- .../ConfigurationClassPostProcessor.java | 224 ++++++++- .../annotation/EarlyBeanReferenceProxy.java | 30 ++ .../EarlyBeanReferenceProxyCreator.java | 256 ++++++++++ .../context/annotation/Feature.java | 34 ++ .../context/annotation/FeatureAnnotation.java | 45 ++ .../annotation/FeatureAnnotationParser.java | 61 +++ .../annotation/FeatureConfiguration.java | 54 ++ .../FeatureMethodExecutionException.java | 28 ++ .../annotation/ProxyCreationException.java | 26 + .../annotation/SimpleComponentRegistrar.java | 46 ++ .../config/AbstractFeatureSpecification.java | 77 +++ .../config/AbstractSpecificationExecutor.java | 54 ++ .../context/config/AdviceMode.java | 28 ++ .../context/config/ExecutorContext.java | 81 +++ .../context/config/FeatureSpecification.java | 127 +++++ .../config/FeatureSpecificationExecutor.java | 41 ++ .../config/SourceAwareSpecification.java | 35 ++ .../support/ConversionServiceFactoryBean.java | 28 +- .../DefaultFormattingConversionService.java | 135 +++++ ...ormattingConversionServiceFactoryBean.java | 99 ++-- .../support/ScriptFactoryPostProcessor.java | 2 +- .../context/config/spring-context-3.1.xsd | 1 + .../example/scannable/FooServiceImpl.java | 1 + .../test/java/example/scannable/_package.java | 25 + .../CustomScopeAnnotationBean.java | 25 + .../example/scannable_scoped/MyScope.java | 25 + ...FactoryAwareFeatureConfigurationTests.java | 67 +++ ...mponentScanAnnotationIntegrationTests.java | 249 ++++++++++ .../ComponentScanAnnotationTests.java | 17 +- .../ComponentScanExecutorTests.java | 67 +++ .../annotation/ComponentScanFeatureTests.java | 55 +++ .../annotation/ComponentScanSpecTests.java | 411 ++++++++++++++++ .../EarlyBeanReferenceProxyCreatorTests.java | 316 ++++++++++++ .../FeatureConfigurationClassTests.java | 52 ++ ...nfigurationImportResourceTests-context.xml | 10 + ...atureConfigurationImportResourceTests.java | 69 +++ .../FeatureConfigurationImportTests.java | 105 ++++ .../FeatureMethodBeanReferenceTests.java | 76 +++ .../FeatureMethodEarlyBeanProxyTests.java | 192 ++++++++ ...tureMethodQualifiedBeanReferenceTests.java | 70 +++ .../SimpleFeatureMethodProcessingTests.java | 64 +++ .../ConfigurationClassProcessingTests.java | 14 +- ...assWithPlaceholderConfigurerBeanTests.java | 119 +++++ .../FeatureMethodAndAutowiredFieldTests.xml | 8 + .../FeatureMethodLifecycleIssueTestSuite.java | 36 ++ .../configuration/StubSpecification.java | 46 ++ .../joda/JodaTimeFormattingTests.java | 4 +- .../format/number/NumberFormattingTests.java | 6 +- .../FormattingConversionServiceTests.java | 11 +- .../validation/DataBinderTests.java | 21 +- .../support/ConversionServiceFactory.java | 74 +-- .../support/DefaultConversionService.java | 86 ++++ .../core/env/AbstractPropertyResolver.java | 7 +- .../springframework/core/env/Environment.java | 2 +- .../core/type/MethodMetadata.java | 5 + .../core/type/StandardMethodMetadata.java | 6 +- .../AnnotationMetadataReadingVisitor.java | 59 ++- .../MethodMetadataReadingVisitor.java | 9 +- .../org/springframework/util/ObjectUtils.java | 51 +- .../springframework/util/ReflectionUtils.java | 14 + .../support/DefaultConversionTests.java | 4 +- .../GenericConversionServiceTests.java | 29 +- .../util/ObjectUtilsTests.java | 40 +- .../spel/support/StandardTypeConverter.java | 6 +- ...essionTestsUsingCoreConversionService.java | 6 +- .../context/annotation/FeatureTestSuite.java | 68 +++ .../support/BeanFactoryTypeConverter.java | 21 +- .../AnnotationDrivenBeanDefinitionParser.java | 114 ++--- ...ransactionManagerBeanDefinitionParser.java | 2 +- .../config/TxAdviceBeanDefinitionParser.java | 60 ++- .../config/TxAnnotationDriven.java | 198 ++++++++ .../config/TxAnnotationDrivenExecutor.java | 139 ++++++ .../config/TxNamespaceHandler.java | 11 - ...ationTransactionNamespaceHandlerTests.java | 1 + .../TxAnnotationDrivenFeatureTests.java | 147 ++++++ ...ttpRequestHandlerBeanDefinitionParser.java | 57 --- .../AnnotationDrivenBeanDefinitionParser.java | 282 ++--------- ...ultServletHandlerBeanDefinitionParser.java | 67 ++- .../servlet/config/MvcAnnotationDriven.java | 249 ++++++++++ .../config/MvcAnnotationDrivenExecutor.java | 235 +++++++++ .../config/MvcDefaultServletHandler.java | 84 ++++ .../MvcDefaultServletHandlerExecutor.java | 82 ++++ .../web/servlet/config/MvcResources.java | 179 +++++++ .../servlet/config/MvcResourcesExecutor.java | 85 ++++ .../servlet/config/MvcViewControllers.java | 90 ++++ .../config/MvcViewControllersExecutor.java | 89 ++++ .../config/ResourcesBeanDefinitionParser.java | 119 ++--- .../ViewControllerBeanDefinitionParser.java | 93 +--- .../web/servlet/config/spring-mvc-3.1.xsd | 10 +- ...tationDrivenBeanDefinitionParserTests.java | 21 +- .../MvcAnnotationDrivenFeatureTests.java | 101 ++++ .../config/MvcDefaultServletHandlerTests.java | 61 +++ .../web/servlet/config/MvcResourcesTests.java | 98 ++++ .../config/MvcViewControllersTests.java | 131 +++++ .../servlet/mvc/annotation/Spr7766Tests.java | 20 +- .../servlet/mvc/annotation/Spr7839Tests.java | 20 +- ...config-message-converters-defaults-off.xml | 15 + .../springframework/http/MediaTypeTests.java | 14 +- spring-framework-reference/src/mvc.xml | 6 +- 127 files changed, 7397 insertions(+), 1132 deletions(-) create mode 100644 org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.java create mode 100644 org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.java create mode 100644 org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java create mode 100644 org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java create mode 100644 org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java create mode 100644 org.springframework.context/src/test/java/example/scannable/_package.java create mode 100644 org.springframework.context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java create mode 100644 org.springframework.context/src/test/java/example/scannable_scoped/MyScope.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java create mode 100644 org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java create mode 100644 org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java create mode 100644 org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java create mode 100644 org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java create mode 100644 org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java create mode 100644 org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java delete mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java create mode 100644 org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java create mode 100644 org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml diff --git a/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java b/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java index ae9888dca4d..c3985b8934e 100644 --- a/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java +++ b/org.springframework.aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java @@ -16,12 +16,12 @@ 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.parsing.ComponentRegistrar; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Element; /** * Utility class for handling registration of auto-proxy creators used internally @@ -52,6 +52,11 @@ public abstract class AopNamespaceUtils { private static final String EXPOSE_PROXY_ATTRIBUTE = "expose-proxy"; + /** + * @deprecated since Spring 3.1 in favor of + * {@link #registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry, ComponentRegistrar, Object, Boolean, Boolean)} + */ + @Deprecated public static void registerAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { @@ -61,6 +66,20 @@ public abstract class AopNamespaceUtils { registerComponentIfNecessary(beanDefinition, parserContext); } + public static void registerAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, ComponentRegistrar parserContext, Object source, Boolean proxyTargetClass, Boolean exposeProxy) { + + BeanDefinition beanDefinition = + AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry, source); + useClassProxyingIfNecessary(registry, proxyTargetClass, exposeProxy); + registerComponentIfNecessary(beanDefinition, parserContext); + } + + public static void registerAutoProxyCreatorIfNecessary( + BeanDefinitionRegistry registry, ComponentRegistrar parserContext, Object source, Boolean proxyTargetClass) { + registerAutoProxyCreatorIfNecessary(registry, parserContext, source, proxyTargetClass, false); + } + public static void registerAspectJAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { @@ -101,6 +120,12 @@ public abstract class AopNamespaceUtils { } + /** + * @deprecated since Spring 3.1 in favor of + * {@link #useClassProxyingIfNecessary(BeanDefinitionRegistry, Boolean, Boolean)} + * which does not require a parameter of type org.w3c.dom.Element + */ + @Deprecated private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, Element sourceElement) { if (sourceElement != null) { boolean proxyTargetClass = Boolean.valueOf(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); @@ -114,11 +139,20 @@ public abstract class AopNamespaceUtils { } } - private static void registerComponentIfNecessary(BeanDefinition beanDefinition, ParserContext parserContext) { + private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, Boolean proxyTargetClass, Boolean exposeProxy) { + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } + + private static void registerComponentIfNecessary(BeanDefinition beanDefinition, ComponentRegistrar componentRegistrar) { if (beanDefinition != null) { BeanComponentDefinition componentDefinition = new BeanComponentDefinition(beanDefinition, AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME); - parserContext.registerComponent(componentDefinition); + componentRegistrar.registerComponent(componentDefinition); } } diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java b/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java index 7c5ba8295ab..dcbe5a54e51 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -86,7 +86,7 @@ public abstract class BeanUtils { } /** - * Convenience method to instantiate a class using its no-arg constructor. + * Instantiate a class using its no-arg constructor. * As this method doesn't try to load classes by name, it should avoid * class-loading issues. *

Note that this method tries to set the constructor accessible @@ -108,6 +108,27 @@ public abstract class BeanUtils { } } + /** + * Instantiate a class using its no-arg constructor and return the new instance + * as the 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. + *

As this method doesn't try to load classes by name, it should avoid + * class-loading issues. + *

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 + */ + @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. * As this method doesn't try to load classes by name, it should avoid diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.java new file mode 100644 index 00000000000..9184b9c0a3b --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/BeanDefinitionRegistrar.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.parsing; + +import org.springframework.beans.factory.config.BeanDefinition; + +public interface BeanDefinitionRegistrar { + + String registerWithGeneratedName(BeanDefinition beanDefinition); + +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.java new file mode 100644 index 00000000000..94eb6541b62 --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ComponentRegistrar.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.parsing; + +public interface ComponentRegistrar extends BeanDefinitionRegistrar { + + void registerBeanComponent(BeanComponentDefinition component); + + void registerComponent(ComponentDefinition component); +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java index 64689f6486c..39c9741a971 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java @@ -30,12 +30,13 @@ public class ReaderContext { private final Resource resource; - private final ProblemReporter problemReporter; - private final ReaderEventListener eventListener; private final SourceExtractor sourceExtractor; + // TODO SPR-7420: review exposing problem reporter + protected final ProblemReporter problemReporter; + public ReaderContext(Resource resource, ProblemReporter problemReporter, ReaderEventListener eventListener, SourceExtractor sourceExtractor) { diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java new file mode 100644 index 00000000000..2f90f761fae --- /dev/null +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/parsing/SimpleProblemCollector.java @@ -0,0 +1,59 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.parsing; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.io.DescriptiveResource; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public class SimpleProblemCollector { + + private Location location = null; + private List errors = new ArrayList(); + + public SimpleProblemCollector(Object location) { + if (location != null) { + this.location = new Location(new DescriptiveResource(location.toString())); + } + } + + public void error(String message) { + this.errors.add(new Problem(message, this.location)); + } + + public void error(String message, Throwable cause) { + this.errors.add(new Problem(message, this.location, null, cause)); + } + + public void reportProblems(ProblemReporter reporter) { + for (Problem error : errors) { + reporter.error(error); + } + } + + public boolean hasErrors() { + return this.errors.size() > 0; + } + +} diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java index 478697fffcf..3b338d5b27b 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java index 1d80a290cfc..26603663fd3 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java @@ -166,7 +166,7 @@ public class BeanDefinitionReaderUtils { * for the given bean definition or the definition cannot be registered */ public static String registerWithGeneratedName( - AbstractBeanDefinition definition, BeanDefinitionRegistry registry) + BeanDefinition definition, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { String generatedName = generateBeanName(definition, registry, false); diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java index 93ecbb44bb5..2f352d9dddf 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -21,9 +21,8 @@ import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; /** - * Interface used by the - * {@link org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader} to - * handle custom, top-level (directly under <beans>) tags. + * 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. @@ -31,11 +30,19 @@ import org.springframework.beans.factory.config.BeanDefinition; *

The parser locates a {@link BeanDefinitionParser} from the associated * {@link NamespaceHandler} for the namespace in which the custom tag resides. * + *

Implementations are encouraged to decouple XML parsing from bean registration by + * parsing element(s) into a {@link org.springframework.context.FeatureSpecification + * FeatureSpecification} object and subsequently executing that specification. + * Doing so allows for maximum reuse between XML-based and annotation-based + * configuration options. + * * @author Rob Harrop * @since 2.0 * @see NamespaceHandler - * @see org.springframework.beans.factory.xml.BeanDefinitionDecorator * @see AbstractBeanDefinitionParser + * @see org.springframework.beans.factory.xml.BeanDefinitionDecorator + * @see org.springframework.context.FeatureSpecification + * @see org.springframework.context.AbstractSpecificationExecutor */ public interface BeanDefinitionParser { diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java index c36314c0d14..78ad74e2f2c 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java @@ -21,6 +21,7 @@ import java.util.Stack; 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.ComponentRegistrar; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -36,7 +37,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; * @see XmlReaderContext * @see BeanDefinitionParserDelegate */ -public final class ParserContext { +public final class ParserContext implements ComponentRegistrar { private final XmlReaderContext readerContext; @@ -121,4 +122,8 @@ public final class ParserContext { registerComponent(component); } + public String registerWithGeneratedName(BeanDefinition beanDefinition) { + return this.readerContext.registerWithGeneratedName(beanDefinition); + } + } diff --git a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java index cbb5a0d5040..8dd361ce399 100644 --- a/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java +++ b/org.springframework.beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java @@ -83,4 +83,9 @@ public class XmlReaderContext extends ReaderContext { return generatedName; } + // TODO SPR-7420: review exposing problem reporter + public ProblemReporter getProblemReporter() { + return this.problemReporter; + } + } diff --git a/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index eef452f0ee9..f65f0ae2927 100644 --- a/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/org.springframework.beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -16,6 +16,15 @@ package org.springframework.beans.factory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import java.lang.reflect.Field; import java.net.MalformedURLException; import java.security.AccessControlContext; @@ -31,20 +40,13 @@ import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; + import javax.security.auth.Subject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import static org.junit.Assert.*; import org.junit.Ignore; import org.junit.Test; -import test.beans.DerivedTestBean; -import test.beans.DummyFactory; -import test.beans.ITestBean; -import test.beans.LifecycleBean; -import test.beans.NestedTestBean; -import test.beans.TestBean; - import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.NotWritablePropertyException; @@ -75,12 +77,19 @@ import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +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.util.StopWatch; +import test.beans.DerivedTestBean; +import test.beans.DummyFactory; +import test.beans.ITestBean; +import test.beans.LifecycleBean; +import test.beans.NestedTestBean; +import test.beans.TestBean; + /** * Tests properties population and autowire behavior. * @@ -849,7 +858,7 @@ public class DefaultListableBeanFactoryTests { @Test public void testCustomConverter() { DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); - GenericConversionService conversionService = (GenericConversionService) ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new Converter() { public Float convert(String source) { try { diff --git a/org.springframework.context/.springBeans b/org.springframework.context/.springBeans index 84849427f41..c36c1e5de08 100644 --- a/org.springframework.context/.springBeans +++ b/org.springframework.context/.springBeans @@ -11,6 +11,7 @@ src/test/java/org/springframework/context/annotation/configuration/SecondLevelSubConfig-context.xml src/test/java/org/springframework/context/annotation/configuration/ImportXmlWithAopNamespace-context.xml src/test/java/org/springframework/context/annotation/Spr6602Tests-context.xml + src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index fd8b5dc25b8..334cc233f12 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -141,5 +141,4 @@ public class AnnotatedBeanDefinitionReader { definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); } - } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java index 1c201527b94..62f07731908 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/AnnotationScopeMetadataResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -37,10 +37,9 @@ import org.springframework.util.Assert; */ public class AnnotationScopeMetadataResolver implements ScopeMetadataResolver { - private Class scopeAnnotationType = Scope.class; - - private final ScopedProxyMode defaultProxyMode; + protected Class scopeAnnotationType = Scope.class; + private final ScopedProxyMode defaultProxyMode; /** * Create a new instance of the AnnotationScopeMetadataResolver class. diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java new file mode 100644 index 00000000000..f44d7b50518 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java @@ -0,0 +1,51 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.reflect.Method; + +import org.springframework.core.annotation.AnnotationUtils; + +/** + * Utilities for processing {@link Bean}-annotated methods. + * + * @author Chris Beams + * @since 3.1 + */ +class BeanAnnotationHelper { + + /** + * Return whether the given method is annotated directly or indirectly with @Bean. + */ + public static boolean isBeanAnnotated(Method method) { + return AnnotationUtils.findAnnotation(method, Bean.class) != null; + } + + public static String determineBeanNameFor(Method beanMethod) { + // by default the bean name is the name of the @Bean-annotated method + String beanName = beanMethod.getName(); + + // check to see if the user has explicitly set the bean name + Bean bean = AnnotationUtils.findAnnotation(beanMethod, Bean.class); + if (bean != null && bean.name().length > 0) { + beanName = bean.name()[0]; + } + + return beanName; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index 4f780ef343b..9488edf117c 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -208,9 +208,10 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo *

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 number of beans registered + * @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); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index 9ea48239b14..459d7b271cf 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -69,8 +69,7 @@ import org.springframework.util.ClassUtils; */ public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { - private static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; - + static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; protected final Log logger = LogFactory.getLog(getClass()); diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java index 043a485ea3c..c45ca50401f 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -16,59 +16,137 @@ 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.BeanNameGenerator; - +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. + * Configures component scanning directives for use with {@link Configuration @Configuration} + * classes. Provides support parallel with Spring XML's {@code } + * element. * - * TODO SPR-7508: complete documentation. + *

One of {@link #basePackageClasses()}, {@link #basePackages()} or its alias {@link #value()} + * must be specified. + * + *

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. * * @author Chris Beams * @since 3.1 - * @see FilterType - * @see Configuration */ +@Documented +@FeatureAnnotation(parser=ComponentScanAnnotationParser.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ComponentScan { - /** base packages to scan */ + /** + * Alias for the {@link #basePackages()} attribute. + * Allows for more concise annotation declarations e.g.: + * {@code @ComponentScan("org.my.pkg")} instead of + * {@code @ComponentScan(basePackages="org.my.pkg")}. + */ String[] value() default {}; - Class[] packageOf() default Void.class; + /** + * 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. + */ + 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. + */ Class nameGenerator() default AnnotationBeanNameGenerator.class; + /** + * The {@link ScopeMetadataResolver} to be used for resolving the scope of detected components. + */ Class scopeResolver() default AnnotationScopeMetadataResolver.class; - String resourcePattern() default "**/*.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. + * @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; - IncludeFilter[] includeFilters() default {}; + /** + * 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. + * @see #resourcePattern() + */ + Filter[] includeFilters() default {}; - ExcludeFilter[] excludeFilters() default {}; + /** + * Specifies which types are not eligible for component scanning. + * @see #resourcePattern() + */ + Filter[] excludeFilters() default {}; + /** + * Declares the type filter to be used as an {@linkplain ComponentScan#includeFilters() + * include filter} or {@linkplain ComponentScan#includeFilters() exclude filter}. + */ @Retention(RetentionPolicy.SOURCE) - @interface IncludeFilter { + @interface Filter { + /** + * The type of filter to use. + *

Note that the filter types available are limited to those that may + * be expressed as a {@code Class} in the {@link #value()} attribute. This is + * in contrast to {@code }, which allows for + * expression-based (i.e., string-based) filters such as AspectJ pointcuts. + * These filter types are intentionally not supported here, and not available + * in the {@link FilterType} enum. + * @see FilterType + */ FilterType type() default FilterType.ANNOTATION; - Class value(); - } - @Retention(RetentionPolicy.SOURCE) - @interface ExcludeFilter { - FilterType type() default FilterType.ANNOTATION; + /** + * The class to use as the filter. In the case of {@link FilterType#ANNOTATION}, + * the class will be the annotation itself. In the case of + * {@link FilterType#ASSIGNABLE_TYPE}, the class will be the type that detected + * components should be assignable to. And in the case of {@link FilterType#CUSTOM}, + * the class will be an implementation of {@link TypeFilter}. + */ Class value(); } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java new file mode 100644 index 00000000000..47c1abedaa9 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanAnnotationParser.java @@ -0,0 +1,97 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.util.Map; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * {@link FeatureAnnotationParser} implementation that reads attributes from a + * {@link ComponentScan @ComponentScan} annotation into a {@link ComponentScanSpec} + * which can in turn be executed by {@link ComponentScanExecutor}. + * {@link ComponentScanBeanDefinitionParser} serves the same role for + * the {@code } XML element. + * + *

Note that {@link ComponentScanSpec} objects may be directly + * instantiated and returned from {@link Feature @Feature} methods as an + * alternative to using the {@link ComponentScan @ComponentScan} annotation. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScan + * @see ComponentScanSpec + * @see ComponentScanExecutor + * @see ComponentScanBeanDefinitionParser + * @see ConfigurationClassBeanDefinitionReader + */ +final class ComponentScanAnnotationParser implements FeatureAnnotationParser { + + /** + * Create and return a new {@link ComponentScanSpec} from the given + * {@link ComponentScan} annotation metadata. + * @throws IllegalArgumentException if ComponentScan attributes are not present in metadata + */ + public ComponentScanSpec process(AnnotationMetadata metadata) { + Map attribs = metadata.getAnnotationAttributes(ComponentScan.class.getName(), true); + Assert.notNull(attribs, String.format("@ComponentScan annotation not found " + + "while parsing metadata for class [%s].", metadata.getClassName())); + + ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + ComponentScanSpec spec = new ComponentScanSpec(); + + for (String pkg : (String[])attribs.get("value")) { + spec.addBasePackage(pkg); + } + for (String pkg : (String[])attribs.get("basePackages")) { + spec.addBasePackage(pkg); + } + for (String className : (String[])attribs.get("basePackageClasses")) { + spec.addBasePackage(className.substring(0, className.lastIndexOf('.'))); + } + + String resolverAttribute = "scopeResolver"; + if (!((String)attribs.get(resolverAttribute)).equals(((Class)AnnotationUtils.getDefaultValue(ComponentScan.class, resolverAttribute)).getName())) { + spec.scopeMetadataResolver((String)attribs.get(resolverAttribute), classLoader); + } + String scopedProxyAttribute = "scopedProxy"; + ScopedProxyMode scopedProxyMode = (ScopedProxyMode) attribs.get(scopedProxyAttribute); + if (scopedProxyMode != ((ScopedProxyMode)AnnotationUtils.getDefaultValue(ComponentScan.class, scopedProxyAttribute))) { + spec.scopedProxyMode(scopedProxyMode); + } + + for (Filter filter : (Filter[]) attribs.get("includeFilters")) { + spec.addIncludeFilter(filter.type().toString(), filter.value().getName(), classLoader); + } + for (Filter filter : (Filter[]) attribs.get("excludeFilters")) { + spec.addExcludeFilter(filter.type().toString(), filter.value().getName(), classLoader); + } + + spec.resourcePattern((String)attribs.get("resourcePattern")) + .useDefaultFilters((Boolean)attribs.get("useDefaultFilters")) + .beanNameGenerator((String)attribs.get("nameGenerator"), classLoader) + .source(metadata.getClassName()) + .sourceName(metadata.getClassName()); + + return spec; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java index ee1a4920224..bb56e713e73 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -16,264 +16,80 @@ package org.springframework.context.annotation; -import java.lang.annotation.Annotation; -import java.util.Set; -import java.util.regex.Pattern; - +import org.springframework.beans.factory.config.BeanDefinition; +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.config.ExecutorContext; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.FatalBeanException; -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.util.StringUtils; - /** - * Parser for the <context:component-scan/> element. - * + * Parser for the {@code } element. Parsed metadata is + * used to populate and execute a {@link ComponentScanSpec} instance. + * * @author Mark Fisher * @author Ramnivas Laddad * @author Juergen Hoeller + * @author Chris Beams * @since 2.5 + * @see ComponentScan + * @see ComponentScanSpec + * @see ComponentScanExecutor */ 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"; - - public BeanDefinition parse(Element element, ParserContext parserContext) { - String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE), - 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) { XmlReaderContext readerContext = parserContext.getReaderContext(); + ClassLoader classLoader = readerContext.getResourceLoader().getClassLoader(); - boolean useDefaultFilters = true; - if (element.hasAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)) { - useDefaultFilters = Boolean.valueOf(element.getAttribute(USE_DEFAULT_FILTERS_ATTRIBUTE)); - } - - // Delegate bean definition registration to scanner class. - ClassPathBeanDefinitionScanner scanner = createScanner(readerContext, useDefaultFilters); - scanner.setResourceLoader(readerContext.getResourceLoader()); - scanner.setEnvironment(parserContext.getDelegate().getEnvironment()); - 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) { - readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); - } - - try { - parseScope(element, scanner); - } - catch (Exception ex) { - readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); - } - - parseTypeFilters(element, scanner, readerContext, parserContext); - - return scanner; - } - - protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) { - return new ClassPathBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters); - } - - 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.valueOf(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, XmlReaderContext readerContext, ParserContext parserContext) { + ComponentScanSpec spec = + ComponentScanSpec.forDelimitedPackages(element.getAttribute("base-package")) + .includeAnnotationConfig(element.getAttribute("annotation-config")) + .useDefaultFilters(element.getAttribute("use-default-filters")) + .resourcePattern(element.getAttribute("resource-pattern")) + .beanNameGenerator(element.getAttribute("name-generator"), classLoader) + .scopeMetadataResolver(element.getAttribute("scope-resolver"), classLoader) + .scopedProxyMode(element.getAttribute("scoped-proxy")); // 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); - scanner.addIncludeFilter(typeFilter); - } - else if (EXCLUDE_FILTER_ELEMENT.equals(localName)) { - TypeFilter typeFilter = createTypeFilter((Element) node, classLoader); - scanner.addExcludeFilter(typeFilter); - } + String filterType = ((Element)node).getAttribute("type"); + String expression = ((Element)node).getAttribute("expression"); + if ("include-filter".equals(localName)) { + spec.addIncludeFilter(filterType, expression, classLoader); } - catch (Exception ex) { - readerContext.error(ex.getMessage(), readerContext.extractSource(element), ex.getCause()); + else if ("exclude-filter".equals(localName)) { + spec.addExcludeFilter(filterType, expression, classLoader); } } } + + spec.beanDefinitionDefaults(parserContext.getDelegate().getBeanDefinitionDefaults()) + .autowireCandidatePatterns(parserContext.getDelegate().getAutowireCandidatePatterns()) + .source(readerContext.extractSource(element)) + .sourceName(element.getTagName()) + .execute(createExecutorContext(parserContext)); + return null; } - @SuppressWarnings("unchecked") - protected TypeFilter createTypeFilter(Element element, ClassLoader classLoader) { - String filterType = element.getAttribute(FILTER_TYPE_ATTRIBUTE); - String expression = element.getAttribute(FILTER_EXPRESSION_ATTRIBUTE); - try { - if ("annotation".equals(filterType)) { - return new AnnotationTypeFilter((Class) classLoader.loadClass(expression)); - } - else if ("assignable".equals(filterType)) { - return new AssignableTypeFilter(classLoader.loadClass(expression)); - } - 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 = classLoader.loadClass(expression); - 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); - } - } - catch (ClassNotFoundException ex) { - throw new FatalBeanException("Type filter class not found: " + expression, ex); - } - } - @SuppressWarnings("unchecked") - private Object instantiateUserDefinedStrategy(String className, Class strategyType, ClassLoader classLoader) { - Object result = null; - try { - result = classLoader.loadClass(className).newInstance(); - } - catch (ClassNotFoundException ex) { - throw new IllegalArgumentException("Class [" + className + "] for strategy [" + - strategyType.getName() + "] not found", ex); - } - catch (Exception 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; + // Adapt the given ParserContext instance into an ExecutorContext. + // TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter utility + // or otherwise unify these two types + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setResourceLoader(parserContext.getReaderContext().getResourceLoader()); + executorContext.setEnvironment(parserContext.getDelegate().getEnvironment()); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java new file mode 100644 index 00000000000..ac146d176f2 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanExecutor.java @@ -0,0 +1,110 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.util.Set; + +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.BeanDefinitionRegistry; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.type.filter.TypeFilter; + +/** + * Executes the {@link ComponentScanSpec} feature specification. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScanSpec + * @see ComponentScanBeanDefinitionParser + * @see ComponentScan + */ +final class ComponentScanExecutor extends AbstractSpecificationExecutor { + + /** + * Configure a {@link ClassPathBeanDefinitionScanner} based on the content of + * the given specification and perform actual scanning and bean definition + * registration. + */ + protected void doExecute(ComponentScanSpec spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ResourceLoader resourceLoader = executorContext.getResourceLoader(); + Environment environment = executorContext.getEnvironment(); + + ClassPathBeanDefinitionScanner scanner = spec.useDefaultFilters() == null ? + new ClassPathBeanDefinitionScanner(registry) : + new ClassPathBeanDefinitionScanner(registry, spec.useDefaultFilters()); + + scanner.setResourceLoader(resourceLoader); + scanner.setEnvironment(environment); + + if (spec.beanDefinitionDefaults() != null) { + scanner.setBeanDefinitionDefaults(spec.beanDefinitionDefaults()); + } + if (spec.autowireCandidatePatterns() != null) { + scanner.setAutowireCandidatePatterns(spec.autowireCandidatePatterns()); + } + + if (spec.resourcePattern() != null) { + scanner.setResourcePattern(spec.resourcePattern()); + } + if (spec.beanNameGenerator() != null) { + scanner.setBeanNameGenerator(spec.beanNameGenerator()); + } + if (spec.includeAnnotationConfig() != null) { + scanner.setIncludeAnnotationConfig(spec.includeAnnotationConfig()); + } + if (spec.scopeMetadataResolver() != null) { + scanner.setScopeMetadataResolver(spec.scopeMetadataResolver()); + } + if (spec.scopedProxyMode() != null) { + scanner.setScopedProxyMode(spec.scopedProxyMode()); + } + for (TypeFilter filter : spec.includeFilters()) { + scanner.addIncludeFilter(filter); + } + for (TypeFilter filter : spec.excludeFilters()) { + scanner.addExcludeFilter(filter); + } + + Set scannedBeans = scanner.doScan(spec.basePackages()); + + Object source = spec.source(); + String sourceName = spec.sourceName(); + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(sourceName, source); + + for (BeanDefinitionHolder beanDefHolder : scannedBeans) { + compositeDef.addNestedComponent(new BeanComponentDefinition(beanDefHolder)); + } + + // Register annotation config processors, if necessary. + if ((spec.includeAnnotationConfig() != null) && spec.includeAnnotationConfig()) { + Set processorDefinitions = + AnnotationConfigUtils.registerAnnotationConfigProcessors(registry, source); + for (BeanDefinitionHolder processorDefinition : processorDefinitions) { + compositeDef.addNestedComponent(new BeanComponentDefinition(processorDefinition)); + } + } + + executorContext.getRegistrar().registerComponent(compositeDef); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java new file mode 100644 index 00000000000..6a85d98b680 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ComponentScanSpec.java @@ -0,0 +1,460 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.beans.factory.support.BeanDefinitionDefaults; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +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.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.StringUtils; + +/** + * Specifies the configuration of Spring's component-scanning feature. + * May be used directly within a {@link Feature @Feature} method, or indirectly + * through the {@link ComponentScan @ComponentScan} annotation. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScan + * @see ComponentScanAnnotationParser + * @see ComponentScanBeanDefinitionParser + * @see ComponentScanExecutor + */ +public final class ComponentScanSpec extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = ComponentScanExecutor.class; + + private Boolean includeAnnotationConfig = null; + private String resourcePattern = null; + private List basePackages = new ArrayList(); + private Object beanNameGenerator = null; + private Object scopeMetadataResolver = null; + private Object scopedProxyMode = null; + private Boolean useDefaultFilters = null; + private List includeFilters = new ArrayList(); + private List excludeFilters = new ArrayList(); + + private BeanDefinitionDefaults beanDefinitionDefaults; + private String[] autowireCandidatePatterns; + + private ClassLoader classLoader; + + /** + * Package-visible constructor for use by {@link ComponentScanBeanDefinitionParser}. + * End users should always call String... or Class... constructors to specify + * base packages. + * + * @see #validate() + */ + ComponentScanSpec() { + super(EXECUTOR_TYPE); + } + + /** + * + * @param basePackages + * @see #forDelimitedPackages(String) + */ + public ComponentScanSpec(String... basePackages) { + this(); + Assert.notEmpty(basePackages, "At least one base package must be specified"); + for (String basePackage : basePackages) { + addBasePackage(basePackage); + } + } + + public ComponentScanSpec(Class... basePackageClasses) { + this(packagesFor(basePackageClasses)); + } + + + public ComponentScanSpec includeAnnotationConfig(Boolean includeAnnotationConfig) { + this.includeAnnotationConfig = includeAnnotationConfig; + return this; + } + + ComponentScanSpec includeAnnotationConfig(String includeAnnotationConfig) { + if (StringUtils.hasText(includeAnnotationConfig)) { + this.includeAnnotationConfig = Boolean.valueOf(includeAnnotationConfig); + } + return this; + } + + Boolean includeAnnotationConfig() { + return this.includeAnnotationConfig; + } + + public ComponentScanSpec resourcePattern(String resourcePattern) { + if (StringUtils.hasText(resourcePattern)) { + this.resourcePattern = resourcePattern; + } + return this; + } + + String resourcePattern() { + return resourcePattern; + } + + ComponentScanSpec addBasePackage(String basePackage) { + if (StringUtils.hasText(basePackage)) { + this.basePackages.add(basePackage); + } + return this; + } + + /** + * Return the set of base packages specified, never {@code null}, never empty + * post-validation. + * @see #doValidate(SimpleProblemReporter) + */ + String[] basePackages() { + return this.basePackages.toArray(new String[this.basePackages.size()]); + } + + public ComponentScanSpec beanNameGenerator(BeanNameGenerator beanNameGenerator) { + this.beanNameGenerator = beanNameGenerator; + return this; + } + + /** + * Set the class name of the BeanNameGenerator to be used and the ClassLoader + * to load it. + */ + ComponentScanSpec beanNameGenerator(String beanNameGenerator, ClassLoader classLoader) { + setClassLoader(classLoader); + if (StringUtils.hasText(beanNameGenerator)) { + this.beanNameGenerator = beanNameGenerator; + } + return this; + } + + BeanNameGenerator beanNameGenerator() { + return nullSafeTypedObject(this.beanNameGenerator, BeanNameGenerator.class); + } + + public ComponentScanSpec scopeMetadataResolver(ScopeMetadataResolver scopeMetadataResolver) { + this.scopeMetadataResolver = scopeMetadataResolver; + return this; + } + + ComponentScanSpec scopeMetadataResolver(String scopeMetadataResolver, ClassLoader classLoader) { + setClassLoader(classLoader); + if (StringUtils.hasText(scopeMetadataResolver)) { + this.scopeMetadataResolver = scopeMetadataResolver; + } + return this; + } + + ScopeMetadataResolver scopeMetadataResolver() { + return nullSafeTypedObject(this.scopeMetadataResolver, ScopeMetadataResolver.class); + } + + public ComponentScanSpec scopedProxyMode(ScopedProxyMode scopedProxyMode) { + this.scopedProxyMode = scopedProxyMode; + return this; + } + + ComponentScanSpec scopedProxyMode(String scopedProxyMode) { + if (StringUtils.hasText(scopedProxyMode)) { + this.scopedProxyMode = scopedProxyMode; + } + return this; + } + + ScopedProxyMode scopedProxyMode() { + return nullSafeTypedObject(this.scopedProxyMode, ScopedProxyMode.class); + } + + public ComponentScanSpec useDefaultFilters(Boolean useDefaultFilters) { + this.useDefaultFilters = useDefaultFilters; + return this; + } + + ComponentScanSpec useDefaultFilters(String useDefaultFilters) { + if (StringUtils.hasText(useDefaultFilters)) { + this.useDefaultFilters = Boolean.valueOf(useDefaultFilters); + } + return this; + } + + Boolean useDefaultFilters() { + return this.useDefaultFilters; + } + + public ComponentScanSpec includeFilters(TypeFilter... includeFilters) { + this.includeFilters.clear(); + for (TypeFilter filter : includeFilters) { + addIncludeFilter(filter); + } + return this; + } + + ComponentScanSpec addIncludeFilter(TypeFilter includeFilter) { + Assert.notNull(includeFilter, "includeFilter must not be null"); + this.includeFilters.add(includeFilter); + return this; + } + + ComponentScanSpec addIncludeFilter(String filterType, String expression, ClassLoader classLoader) { + this.includeFilters.add(new FilterTypeDescriptor(filterType, expression, classLoader)); + return this; + } + + TypeFilter[] includeFilters() { + return this.includeFilters.toArray(new TypeFilter[this.includeFilters.size()]); + } + + public ComponentScanSpec excludeFilters(TypeFilter... excludeFilters) { + this.excludeFilters.clear(); + for (TypeFilter filter : excludeFilters) { + addExcludeFilter(filter); + } + return this; + } + + ComponentScanSpec addExcludeFilter(TypeFilter excludeFilter) { + Assert.notNull(excludeFilter, "excludeFilter must not be null"); + this.excludeFilters.add(excludeFilter); + return this; + } + + ComponentScanSpec addExcludeFilter(String filterType, String expression, ClassLoader classLoader) { + this.excludeFilters.add(new FilterTypeDescriptor(filterType, expression, classLoader)); + return this; + } + + TypeFilter[] excludeFilters() { + return this.excludeFilters.toArray(new TypeFilter[this.excludeFilters.size()]); + } + + ComponentScanSpec beanDefinitionDefaults(BeanDefinitionDefaults beanDefinitionDefaults) { + this.beanDefinitionDefaults = beanDefinitionDefaults; + return this; + } + + BeanDefinitionDefaults beanDefinitionDefaults() { + return this.beanDefinitionDefaults; + } + + ComponentScanSpec autowireCandidatePatterns(String[] autowireCandidatePatterns) { + this.autowireCandidatePatterns = autowireCandidatePatterns; + return this; + } + + String[] autowireCandidatePatterns() { + return this.autowireCandidatePatterns; + } + + + /** + * Create a ComponentScanSpec from a single string containing + * delimited package names. + * @see ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS + */ + static ComponentScanSpec forDelimitedPackages(String basePackages) { + Assert.notNull(basePackages, "base packages must not be null"); + return new ComponentScanSpec( + StringUtils.tokenizeToStringArray(basePackages, + ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); + } + + public void doValidate(SimpleProblemCollector problems) { + if(this.basePackages.isEmpty()) { + problems.error("At least one base package must be specified"); + } + + if(this.beanNameGenerator instanceof String) { + this.beanNameGenerator = instantiateUserDefinedType("bean name generator", BeanNameGenerator.class, this.beanNameGenerator, this.classLoader, problems); + } + + if(this.scopeMetadataResolver instanceof String) { + this.scopeMetadataResolver = instantiateUserDefinedType("scope metadata resolver", ScopeMetadataResolver.class, this.scopeMetadataResolver, this.classLoader, problems); + } + + if (this.scopedProxyMode instanceof String) { + if ("targetClass".equalsIgnoreCase((String)this.scopedProxyMode)) { + this.scopedProxyMode = ScopedProxyMode.TARGET_CLASS; + } + else if ("interfaces".equalsIgnoreCase((String)this.scopedProxyMode)) { + this.scopedProxyMode = ScopedProxyMode.INTERFACES; + } + else if ("no".equalsIgnoreCase((String)this.scopedProxyMode)) { + this.scopedProxyMode = ScopedProxyMode.NO; + } + else { + problems.error("invalid scoped proxy mode [%s] supported modes are " + + "'no', 'interfaces' and 'targetClass'"); + this.scopedProxyMode = null; + } + } + + if (this.scopeMetadataResolver != null && this.scopedProxyMode != null) { + problems.error("Cannot define both scope metadata resolver and scoped proxy mode"); + } + + for (int i = 0; i < this.includeFilters.size(); i++) { + if (this.includeFilters.get(i) instanceof FilterTypeDescriptor) { + this.includeFilters.set(i, ((FilterTypeDescriptor)this.includeFilters.get(i)).createTypeFilter(problems)); + } + } + + for (int i = 0; i < this.excludeFilters.size(); i++) { + if (this.excludeFilters.get(i) instanceof FilterTypeDescriptor) { + this.excludeFilters.set(i, ((FilterTypeDescriptor)this.excludeFilters.get(i)).createTypeFilter(problems)); + } + } + } + + private static Object instantiateUserDefinedType(String description, Class targetType, Object className, ClassLoader classLoader, SimpleProblemCollector problems) { + Assert.isInstanceOf(String.class, className, "userType must be of type String"); + Assert.notNull(classLoader, "classLoader must not be null"); + Assert.notNull(targetType, "targetType must not be null"); + Object instance = null; + try { + instance = classLoader.loadClass((String)className).newInstance(); + if (!targetType.isAssignableFrom(instance.getClass())) { + problems.error(description + " class name must be assignable to " + targetType.getSimpleName()); + instance = null; + } + } + catch (ClassNotFoundException ex) { + problems.error(String.format(description + " class [%s] not found", className), ex); + } + catch (Exception ex) { + problems.error(String.format("Unable to instantiate %s class [%s] for " + + "strategy [%s]. Has a no-argument constructor been provided?", + description, className, targetType.getClass().getSimpleName()), ex); + } + return instance; + } + + private void setClassLoader(ClassLoader classLoader) { + Assert.notNull(classLoader, "classLoader must not be null"); + if (this.classLoader == null) { + this.classLoader = classLoader; + } + else { + Assert.isTrue(this.classLoader == classLoader, "A classLoader has already been assigned " + + "and the supplied classLoader is not the same instance. Use the same classLoader " + + "for all string-based class properties."); + } + } + + @SuppressWarnings("unchecked") + private static T nullSafeTypedObject(Object object, Class type) { + if (object != null) { + if (!(type.isAssignableFrom(object.getClass()))) { + throw new IllegalStateException( + String.format("field must be of type %s but was actually of type %s", type, object.getClass())); + } + } + return (T)object; + } + + private static String[] packagesFor(Class[] classes) { + ArrayList packages = new ArrayList(); + for (Class clazz : classes) { + packages.add(clazz.getPackage().getName()); + } + return packages.toArray(new String[packages.size()]); + } + + + private static class FilterTypeDescriptor { + private String filterType; + private String expression; + private ClassLoader classLoader; + + FilterTypeDescriptor(String filterType, String expression, ClassLoader classLoader) { + Assert.notNull(filterType, "filterType must not be null"); + Assert.notNull(expression, "expression must not be null"); + Assert.notNull(classLoader, "classLoader must not be null"); + this.filterType = filterType; + this.expression = expression; + this.classLoader = classLoader; + } + + @SuppressWarnings("unchecked") + TypeFilter createTypeFilter(SimpleProblemCollector problems) { + try { + if ("annotation".equalsIgnoreCase(this.filterType)) { + return new AnnotationTypeFilter((Class) this.classLoader.loadClass(this.expression)); + } + else if ("assignable".equalsIgnoreCase(this.filterType) + || "assignable_type".equalsIgnoreCase(this.filterType)) { + return new AssignableTypeFilter(this.classLoader.loadClass(this.expression)); + } + else if ("aspectj".equalsIgnoreCase(this.filterType)) { + return new AspectJTypeFilter(this.expression, this.classLoader); + } + else if ("regex".equalsIgnoreCase(this.filterType)) { + return new RegexPatternTypeFilter(Pattern.compile(this.expression)); + } + else if ("custom".equalsIgnoreCase(this.filterType)) { + Class filterClass = this.classLoader.loadClass(this.expression); + if (!TypeFilter.class.isAssignableFrom(filterClass)) { + problems.error(String.format("custom type filter class [%s] must be assignable to %s", + this.expression, TypeFilter.class)); + } + return (TypeFilter) BeanUtils.instantiateClass(filterClass); + } + else { + problems.error(String.format("Unsupported filter type [%s]; supported types are: " + + "'annotation', 'assignable[_type]', 'aspectj', 'regex', 'custom'", this.filterType)); + } + } catch (ClassNotFoundException ex) { + problems.error("Type filter class not found: " + this.expression, ex); + } catch (Exception ex) { + problems.error(ex.getMessage(), ex.getCause()); + } + + return new PlaceholderTypeFilter(); + } + + + private class PlaceholderTypeFilter implements TypeFilter { + + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + throw new UnsupportedOperationException( + String.format("match() method for placeholder type filter for " + + "{filterType=%s,expression=%s} should never be invoked", + filterType, expression)); + } + + } + } + + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java index a277ff9efe2..2c8e6f40a85 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -59,7 +59,7 @@ import org.springframework.stereotype.Component; * @see AnnotationConfigApplicationContext * @see org.springframework.context.annotation.Profile */ -@Target({ElementType.TYPE}) +@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 9aae2ecf32e..22a26ebaf47 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -175,5 +175,6 @@ final class ConfigurationClass { getSimpleName(), count, methodName), new Location(getResource(), getMetadata())); } } - + + } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 238d92bb845..a75a9d14457 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -27,12 +27,13 @@ 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.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.Autowire; import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; @@ -43,23 +44,28 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; import org.springframework.core.Conventions; -import org.springframework.core.annotation.AnnotationUtils; +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.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; /** - * Reads a given fully-populated configuration model, registering bean definitions - * with the given {@link BeanDefinitionRegistry} based on its contents. + * 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 configuration model is not a {@link Resource}. + * not implement/extend any of its artifacts as a set of configuration classes is not a + * {@link Resource}. * * @author Chris Beams * @author Juergen Hoeller @@ -85,6 +91,7 @@ class ConfigurationClassBeanDefinitionReader { private final MetadataReaderFactory metadataReaderFactory; + private ExecutorContext executorContext; /** * Create a new {@link ConfigurationClassBeanDefinitionReader} instance that will be used @@ -92,13 +99,20 @@ class ConfigurationClassBeanDefinitionReader { * @param problemReporter * @param metadataReaderFactory */ - public ConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry, SourceExtractor sourceExtractor, - ProblemReporter problemReporter, MetadataReaderFactory metadataReaderFactory) { + public ConfigurationClassBeanDefinitionReader(final BeanDefinitionRegistry registry, SourceExtractor sourceExtractor, + ProblemReporter problemReporter, MetadataReaderFactory metadataReaderFactory, + ResourceLoader resourceLoader, Environment environment) { this.registry = registry; this.sourceExtractor = sourceExtractor; this.problemReporter = problemReporter; this.metadataReaderFactory = metadataReaderFactory; + this.executorContext = new ExecutorContext(); + this.executorContext.setRegistry(this.registry); + this.executorContext.setRegistrar(new SimpleComponentRegistrar(this.registry)); + this.executorContext.setResourceLoader(resourceLoader); + this.executorContext.setEnvironment(environment); + this.executorContext.setProblemReporter(problemReporter); } @@ -117,15 +131,38 @@ class ConfigurationClassBeanDefinitionReader { * class itself, all its {@link Bean} methods */ private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) { + AnnotationMetadata metadata = configClass.getMetadata(); + processFeatureAnnotations(metadata); doLoadBeanDefinitionForConfigurationClassIfNecessary(configClass); - for (ConfigurationClassMethod method : configClass.getMethods()) { - loadBeanDefinitionsForModelMethod(method); + for (ConfigurationClassMethod beanMethod : configClass.getMethods()) { + loadBeanDefinitionsForBeanMethod(beanMethod); } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); } + private void processFeatureAnnotations(AnnotationMetadata metadata) { + try { + for (String annotationType : metadata.getAnnotationTypes()) { + MetadataReader metadataReader = new SimpleMetadataReaderFactory().getMetadataReader(annotationType); + if (metadataReader.getAnnotationMetadata().isAnnotated(FeatureAnnotation.class.getName())) { + Map annotationAttributes = metadataReader.getAnnotationMetadata().getAnnotationAttributes(FeatureAnnotation.class.getName(), true); + // TODO SPR-7420: this is where we can catch user-defined types and avoid instantiating them for STS purposes + FeatureAnnotationParser processor = (FeatureAnnotationParser) BeanUtils.instantiateClass(Class.forName((String)annotationAttributes.get("parser"))); + FeatureSpecification spec = processor.process(metadata); + spec.execute(this.executorContext); + } + } + } catch (BeanDefinitionParsingException ex) { + throw ex; + } + catch (Exception ex) { + // TODO SPR-7420: what exception to throw? + throw new RuntimeException(ex); + } + } + /** - * Registers the {@link Configuration} class itself as a bean definition. + * Register the {@link Configuration} class itself as a bean definition. */ private void doLoadBeanDefinitionForConfigurationClassIfNecessary(ConfigurationClass configClass) { if (configClass.getBeanName() != null) { @@ -161,9 +198,9 @@ class ConfigurationClassBeanDefinitionReader { * Read a particular {@link ConfigurationClassMethod}, registering bean definitions * with the BeanDefinitionRegistry based on its contents. */ - private void loadBeanDefinitionsForModelMethod(ConfigurationClassMethod method) { - ConfigurationClass configClass = method.getConfigurationClass(); - MethodMetadata metadata = method.getMetadata(); + private void loadBeanDefinitionsForBeanMethod(ConfigurationClassMethod beanMethod) { + ConfigurationClass configClass = beanMethod.getConfigurationClass(); + MethodMetadata metadata = beanMethod.getMetadata(); RootBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass); beanDef.setResource(configClass.getResource()); @@ -176,7 +213,7 @@ class ConfigurationClassBeanDefinitionReader { // consider name and any aliases Map beanAttributes = metadata.getAnnotationAttributes(Bean.class.getName()); List names = new ArrayList(Arrays.asList((String[]) beanAttributes.get("name"))); - String beanName = (names.size() > 0 ? names.remove(0) : method.getMetadata().getMethodName()); + String beanName = (names.size() > 0 ? names.remove(0) : beanMethod.getMetadata().getMethodName()); for (String alias : names) { this.registry.registerAlias(beanName, alias); } @@ -190,7 +227,7 @@ class ConfigurationClassBeanDefinitionReader { // overriding is legal, return immediately if (logger.isDebugEnabled()) { logger.debug(String.format("Skipping loading bean definition for %s: a definition for bean " + - "'%s' already exists. This is likely due to an override in XML.", method, beanName)); + "'%s' already exists. This is likely due to an override in XML.", beanMethod, beanName)); } return; } @@ -255,7 +292,8 @@ class ConfigurationClassBeanDefinitionReader { registry.registerBeanDefinition(beanName, beanDefToRegister); } - + + private void loadBeanDefinitionsFromImportedResources(Map> importedResources) { Map, BeanDefinitionReader> readerInstanceCache = new HashMap, BeanDefinitionReader>(); for (Map.Entry> entry : importedResources.entrySet()) { @@ -357,7 +395,7 @@ class ConfigurationClassBeanDefinitionReader { @Override public boolean isFactoryMethod(Method candidate) { - return (super.isFactoryMethod(candidate) && AnnotationUtils.findAnnotation(candidate, Bean.class) != null); + return (super.isFactoryMethod(candidate) && BeanAnnotationHelper.isBeanAnnotated(candidate)); } @Override @@ -372,14 +410,12 @@ class ConfigurationClassBeanDefinitionReader { * declare at least one {@link Bean @Bean} method. */ private static class InvalidConfigurationImportProblem extends Problem { - public InvalidConfigurationImportProblem(String className, Resource resource, AnnotationMetadata metadata) { - super(String.format("%s was imported as a Configuration class but is not annotated " + - "with @Configuration nor does it declare any @Bean methods. Update the class to " + - "meet either of these requirements or do not attempt to import it.", className), + super(String.format("%s was @Import'ed as a but is not annotated with @FeatureConfiguration or " + + "@Configuration nor does it declare any @Bean methods. Update the class to " + + "meet one of these requirements or do not attempt to @Import it.", className), new Location(resource, metadata)); } - } } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index e3f0ae2938a..174ac4eff08 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -74,7 +74,7 @@ class ConfigurationClassEnhancer { // handling a @Bean-annotated method; otherwise, return index of the NoOp callback. callbackFilter = new CallbackFilter() { public int accept(Method candidateMethod) { - return (AnnotationUtils.findAnnotation(candidateMethod, Bean.class) != null ? 0 : 1); + return (BeanAnnotationHelper.isBeanAnnotated(candidateMethod) ? 0 : 1); } }; } @@ -162,19 +162,21 @@ class ConfigurationClassEnhancer { /** * Enhance a {@link Bean @Bean} method to check the supplied BeanFactory for the * existence of this bean object. + * + * @throws ProxyCreationException if an early bean reference proxy should be + * created but the return type of the bean method being intercepted is not an + * interface and thus not a candidate for JDK proxy creation. + * @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. */ - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - // by default the bean name is the name of the @Bean-annotated method - String beanName = method.getName(); + public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, + MethodProxy cglibMethodProxy) throws ProxyCreationException, Throwable { - // check to see if the user has explicitly set the bean name - Bean bean = AnnotationUtils.findAnnotation(method, Bean.class); - if (bean != null && bean.name().length > 0) { - beanName = bean.name()[0]; - } + String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // determine whether this bean is a scoped-proxy - Scope scope = AnnotationUtils.findAnnotation(method, Scope.class); + Scope scope = AnnotationUtils.findAnnotation(beanMethod, Scope.class); if (scope != null && scope.proxyMode() != ScopedProxyMode.NO) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); if (this.beanFactory.isCurrentlyInCreation(scopedBeanName)) { @@ -206,8 +208,7 @@ class ConfigurationClassEnhancer { return this.beanFactory.getBean(beanName); } - // no cached instance of the bean exists - actually create and return the bean - return proxy.invokeSuper(obj, args); + return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } /** diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java index a176db7ddad..429ba06bce0 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -67,7 +67,7 @@ final class ConfigurationClassMethod { } } } - + @Override public String toString() { return String.format("[%s:name=%s,declaringClass=%s]", diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index aa5681648c8..83dd95beec4 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -16,9 +16,15 @@ package org.springframework.context.annotation; +import static java.lang.String.format; + import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -26,10 +32,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.ListableBeanFactory; 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.DependencyDescriptor; import org.springframework.beans.factory.parsing.FailFastProblemReporter; import org.springframework.beans.factory.parsing.PassThroughSourceExtractor; import org.springframework.beans.factory.parsing.ProblemReporter; @@ -38,12 +46,24 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.EnvironmentAware; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.SourceAwareSpecification; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +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.classreading.SimpleMetadataReaderFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; /** * {@link BeanFactoryPostProcessor} used for bootstrapping processing of @@ -63,7 +83,7 @@ import org.springframework.util.ClassUtils; * @since 3.0 */ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, - BeanClassLoaderAware, EnvironmentAware { + ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { /** Whether the CGLIB2 library is present on the classpath */ private static final boolean cglibAvailable = ClassUtils.isPresent( @@ -76,6 +96,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private ProblemReporter problemReporter = new FailFastProblemReporter(); + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(); @@ -88,6 +110,8 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private Environment environment; + private ConfigurationClassBeanDefinitionReader reader; + /** * Set the {@link SourceExtractor} to use for generated bean definitions @@ -118,6 +142,11 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo this.setMetadataReaderFactoryCalled = true; } + public void setResourceLoader(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.resourceLoader = resourceLoader; + } + public void setBeanClassLoader(ClassLoader beanClassLoader) { this.beanClassLoader = beanClassLoader; if (!this.setMetadataReaderFactoryCalled) { @@ -126,6 +155,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo } public void setEnvironment(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); this.environment = environment; } @@ -143,7 +173,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo "postProcessBeanFactory already called for this post-processor"); } this.postProcessBeanDefinitionRegistryCalled = true; - processConfigBeanDefinitions(registry); + processConfigurationClasses(registry); } /** @@ -158,18 +188,196 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo this.postProcessBeanFactoryCalled = true; if (!this.postProcessBeanDefinitionRegistryCalled) { // BeanDefinitionRegistryPostProcessor hook apparently not supported... - // Simply call processConfigBeanDefinitions lazily at this point then. - processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory); + // Simply call processConfigurationClasses lazily at this point then. + processConfigurationClasses((BeanDefinitionRegistry)beanFactory); } - enhanceConfigurationClasses(beanFactory); } + /** + * Find and process all @Configuration classes with @Feature methods in the given registry. + */ + private void processConfigurationClasses(BeanDefinitionRegistry registry) { + ConfigurationClassBeanDefinitionReader reader = getConfigurationClassBeanDefinitionReader(registry); + processConfigBeanDefinitions(registry, reader); + enhanceConfigurationClasses((ConfigurableListableBeanFactory)registry); + processFeatureConfigurationClasses((ConfigurableListableBeanFactory) registry); + } + + /** + * Process any @FeatureConfiguration classes + */ + private void processFeatureConfigurationClasses(final ConfigurableListableBeanFactory beanFactory) { + Map featureConfigBeans = retrieveFeatureConfigurationBeans(beanFactory); + + if (featureConfigBeans.size() == 0) { + return; + } + + for (final Object featureConfigBean : featureConfigBeans.values()) { + checkForBeanMethods(featureConfigBean.getClass()); + } + + if (!cglibAvailable) { + throw new IllegalStateException("CGLIB is required to process @FeatureConfiguration classes. " + + "Either add CGLIB to the classpath or remove the following @FeatureConfiguration bean definitions: " + + featureConfigBeans.keySet()); + } + + final EarlyBeanReferenceProxyCreator proxyCreator = new EarlyBeanReferenceProxyCreator(beanFactory); + final ExecutorContext executorContext = createExecutorContext(beanFactory); + + for (final Object featureConfigBean : featureConfigBeans.values()) { + ReflectionUtils.doWithMethods(featureConfigBean.getClass(), + new ReflectionUtils.MethodCallback() { + public void doWith(Method featureMethod) throws IllegalArgumentException, IllegalAccessException { + processFeatureMethod(featureMethod, featureConfigBean, executorContext, proxyCreator); + } }, + new ReflectionUtils.MethodFilter() { + public boolean matches(Method candidateMethod) { + return candidateMethod.isAnnotationPresent(Feature.class); + } }); + } + } + + /** + * Alternative to {@link ListableBeanFactory#getBeansWithAnnotation(Class)} that avoids + * instantiating FactoryBean objects. FeatureConfiguration types cannot be registered as + * FactoryBeans, so ignoring them won't cause a problem. On the other hand, using gBWA() + * at this early phase of the container would cause all @Bean methods to be invoked, as they + * are ultimately FactoryBeans underneath. + */ + private Map retrieveFeatureConfigurationBeans(ConfigurableListableBeanFactory beanFactory) { + Map fcBeans = new HashMap(); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName); + if (isFeatureConfiguration(beanDef)) { + fcBeans.put(beanName, beanFactory.getBean(beanName)); + } + } + return fcBeans; + } + + private boolean isFeatureConfiguration(BeanDefinition candidate) { + if (!(candidate instanceof AbstractBeanDefinition) || (candidate.getBeanClassName() == null)) { + return false; + } + AbstractBeanDefinition beanDef = (AbstractBeanDefinition) candidate; + if (beanDef.hasBeanClass()) { + Class beanClass = beanDef.getBeanClass(); + if (AnnotationUtils.findAnnotation(beanClass, FeatureConfiguration.class) != null) { + return true; + } + } + else { + // in the case of @FeatureConfiguration classes included with @Import the bean class name + // will still be in String form. Since we don't know whether the current bean definition + // is a @FeatureConfiguration or not, carefully check for the annotation using ASM instead + // eager classloading. + String className = null; + try { + className = beanDef.getBeanClassName(); + MetadataReader metadataReader = new SimpleMetadataReaderFactory().getMetadataReader(className); + AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata(); + if (annotationMetadata.isAnnotated(FeatureConfiguration.class.getName())) { + return true; + } + } + catch (IOException ex) { + throw new IllegalStateException("Could not create MetadataReader for class " + className, ex); + } + } + return false; + } + + private void checkForBeanMethods(final Class featureConfigClass) { + ReflectionUtils.doWithMethods(featureConfigClass, + new ReflectionUtils.MethodCallback() { + public void doWith(Method beanMethod) throws IllegalArgumentException, IllegalAccessException { + throw new FeatureMethodExecutionException( + format("@FeatureConfiguration classes must not contain @Bean-annotated methods. " + + "%s.%s() is annotated with @Bean and must be removed in order to proceed. " + + "Consider moving this method into a dedicated @Configuration class and " + + "injecting the bean as a parameter into any @Feature method(s) that need it.", + beanMethod.getDeclaringClass().getSimpleName(), beanMethod.getName())); + } }, + new ReflectionUtils.MethodFilter() { + public boolean matches(Method candidateMethod) { + return BeanAnnotationHelper.isBeanAnnotated(candidateMethod); + } }); + } + + /** + * TODO SPR-7420: this method invokes user-supplied code, which is not going to fly for STS + * + * consider introducing some kind of check to see if we're in a tooling context and make guesses + * based on return type rather than actually invoking the method and processing the the specification + * object that returns. + * @param beanFactory + * @throws SecurityException + */ + private void processFeatureMethod(final Method featureMethod, Object configInstance, + ExecutorContext executorContext, EarlyBeanReferenceProxyCreator proxyCreator) { + try { + // get the return type + if (!(FeatureSpecification.class.isAssignableFrom(featureMethod.getReturnType()))) { + // TODO SPR-7420: raise a Problem instead? + throw new IllegalArgumentException( + "return type from @Feature methods must be assignable to FeatureSpecification"); + } + + List beanArgs = new ArrayList(); + Class[] parameterTypes = featureMethod.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + MethodParameter mp = new MethodParameter(featureMethod, i); + DependencyDescriptor dd = new DependencyDescriptor(mp, true, false); + Object proxiedBean = proxyCreator.createProxy(dd); + beanArgs.add(proxiedBean); + } + + // reflectively invoke that method + FeatureSpecification spec; + featureMethod.setAccessible(true); + spec = (FeatureSpecification) featureMethod.invoke(configInstance, beanArgs.toArray(new Object[beanArgs.size()])); + + Assert.notNull(spec, + format("The specification returned from @Feature method %s.%s() must not be null", + featureMethod.getDeclaringClass().getSimpleName(), featureMethod.getName())); + + if (spec instanceof SourceAwareSpecification) { + ((SourceAwareSpecification)spec).source(featureMethod); + ((SourceAwareSpecification)spec).sourceName(featureMethod.getName()); + } + spec.execute(executorContext); + } catch (Exception ex) { + throw new FeatureMethodExecutionException(ex); + } + } + + private ExecutorContext createExecutorContext(ConfigurableListableBeanFactory beanFactory) { + final BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setEnvironment(this.environment); + executorContext.setResourceLoader(this.resourceLoader); + executorContext.setRegistry(registry); + executorContext.setRegistrar(new SimpleComponentRegistrar(registry)); + // TODO SPR-7420: how to get hold of the current problem reporter here? + executorContext.setProblemReporter(new FailFastProblemReporter()); + return executorContext; + } + + private ConfigurationClassBeanDefinitionReader getConfigurationClassBeanDefinitionReader(BeanDefinitionRegistry registry) { + if (this.reader == null) { + this.reader = new ConfigurationClassBeanDefinitionReader( + registry, this.sourceExtractor, this.problemReporter, this.metadataReaderFactory, this.resourceLoader, this.environment); + } + return this.reader; + } /** * Build and validate a configuration model based on the registry of * {@link Configuration} classes. */ - public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { + public void processConfigBeanDefinitions(BeanDefinitionRegistry registry, ConfigurationClassBeanDefinitionReader reader) { Set configCandidates = new LinkedHashSet(); for (String beanName : registry.getBeanDefinitionNames()) { BeanDefinition beanDef = registry.getBeanDefinition(beanName); @@ -202,8 +410,6 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo parser.validate(); // Read the model and create bean definitions based on its content - ConfigurationClassBeanDefinitionReader reader = new ConfigurationClassBeanDefinitionReader( - registry, this.sourceExtractor, this.problemReporter, this.metadataReaderFactory); reader.loadBeanDefinitions(parser.getConfigurationClasses()); } diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java new file mode 100644 index 00000000000..c0cd0965496 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxy.java @@ -0,0 +1,30 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +/** + * Marker interface indicating that an object is a proxy for a bean referenced + * from within a {@link Feature @Feature} method. + * + * @author Chris Beams + * @since 3.1 + */ +public interface EarlyBeanReferenceProxy { + + Object dereferenceTargetBean(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java new file mode 100644 index 00000000000..121be542bd8 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreator.java @@ -0,0 +1,256 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Creates proxies for beans referenced from within @Feature methods. + * + * TODO SPR-7420: document + * - discuss why proxies are important (avoiding side effects of early instantiation) + * - discuss benefits of interface-based proxies over concrete proxies + * - make it clear that both of the above are possible + * - discuss invocation of @Bean methods and how they too return proxies. + * this 'proxy returning a proxy' approach can be confusing at first, but the + * implementation should help in making it clear. + * + * @author Chris Beams + * @since 3.1 + */ +class EarlyBeanReferenceProxyCreator { + + static final String FINAL_CLASS_ERROR_MESSAGE = + "Cannot create subclass proxy for bean type %s because it is a final class. " + + "Make the class non-final or inject the bean by interface rather than by concrete class."; + + static final String MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE = + "Cannot create subclass proxy for bean type %s because it does not have a no-arg constructor. " + + "Add a no-arg constructor or attempt to inject the bean by interface rather than by concrete class."; + + static final String PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE = + "Cannot create subclass proxy for bean type %s because its no-arg constructor is private. " + + "Increase the visibility of the no-arg constructor or attempt to inject the bean by interface rather " + + "than by concrete class."; + + private final AutowireCapableBeanFactory beanFactory; + + + /** + * Create a new proxy creator that will dereference proxy target beans against + * the given bean factory. + */ + public EarlyBeanReferenceProxyCreator(AutowireCapableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + /** + * Create a proxy that will ultimately dereference its target object using + * the given dependency descriptor. + */ + public Object createProxy(DependencyDescriptor descriptor) { + return doCreateProxy(new ResolveDependencyTargetBeanDereferencingInterceptor(descriptor)); + } + + /** + * Create a proxy that looks up target beans using the given dereferencing interceptor. + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private Object doCreateProxy(TargetBeanDereferencingInterceptor targetBeanDereferencingInterceptor) { + Enhancer enhancer = new Enhancer(); + Class targetBeanType = targetBeanDereferencingInterceptor.getTargetBeanType(); + if (targetBeanType.isInterface()) { + enhancer.setSuperclass(Object.class); + enhancer.setInterfaces(new Class[] {targetBeanType, EarlyBeanReferenceProxy.class}); + } else { + assertClassIsProxyCapable(targetBeanType); + enhancer.setSuperclass(targetBeanType); + enhancer.setInterfaces(new Class[] {EarlyBeanReferenceProxy.class}); + } + enhancer.setCallbacks(new Callback[] { + new BeanMethodInterceptor(), + new ObjectMethodsInterceptor(), + targetBeanDereferencingInterceptor, + new TargetBeanDelegatingMethodInterceptor() + }); + enhancer.setCallbackFilter(new CallbackFilter() { + public int accept(Method method) { + if (BeanAnnotationHelper.isBeanAnnotated(method)) { + return 0; + } + if (ReflectionUtils.isObjectMethod(method)) { + return 1; + } + if (method.getName().equals("dereferenceTargetBean")) { + return 2; + } + return 3; + } + }); + return enhancer.create(); + } + + /** + * Return whether the given class is capable of being subclass proxied by CGLIB. + */ + private static void assertClassIsProxyCapable(Class clazz) { + Assert.isTrue(!clazz.isInterface(), "class parameter must be a concrete type"); + if ((clazz.getModifiers() & Modifier.FINAL) != 0) { + throw new ProxyCreationException(String.format(FINAL_CLASS_ERROR_MESSAGE, clazz.getName())); + } + try { + // attempt to retrieve the no-arg constructor for the class + Constructor noArgCtor = clazz.getDeclaredConstructor(); + if ((noArgCtor.getModifiers() & Modifier.PRIVATE) != 0) { + throw new ProxyCreationException(String.format(PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, clazz.getName())); + } + } catch (NoSuchMethodException ex) { + throw new ProxyCreationException(String.format(MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, clazz.getName())); + } + } + + + /** + * Interceptor for @Bean-annotated methods called from early-proxied bean instances, such as + * @Configuration class instances. Invoking instance methods on early-proxied beans usually + * causes an eager bean lookup, but in the case of @Bean methods, it is important to return + * a proxy. + */ + private class BeanMethodInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, final Method beanMethod, Object[] args, MethodProxy proxy) throws Throwable { + return doCreateProxy(new ByNameLookupTargetBeanDereferencingInterceptor(beanMethod)); + } + + } + + + /** + * Interceptor for methods declared by java.lang.Object() + */ + private static class ObjectMethodsInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + if (method.getName().equals("toString")) { + return String.format("EarlyBeanReferenceProxy for bean of type %s", + obj.getClass().getSuperclass().getSimpleName()); + } + if (method.getName().equals("hashCode")) { + return System.identityHashCode(obj); + } + if (method.getName().equals("equals")) { + return obj == args[0]; + } + if (method.getName().equals("finalize")) { + return null; + } + return proxy.invokeSuper(obj, args); + } + + } + + + /** + * Strategy interface allowing for various approaches to dereferencing (i.e. 'looking up') + * the target bean for an early bean reference proxy. + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private interface TargetBeanDereferencingInterceptor extends MethodInterceptor { + Class getTargetBeanType(); + } + + + /** + * Interceptor that dereferences the target bean for the proxy by calling + * {@link AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)}. + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private class ResolveDependencyTargetBeanDereferencingInterceptor implements TargetBeanDereferencingInterceptor { + + private final DependencyDescriptor descriptor; + + public ResolveDependencyTargetBeanDereferencingInterceptor(DependencyDescriptor descriptor) { + this.descriptor = descriptor; + } + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return beanFactory.resolveDependency(descriptor, null); + } + + public Class getTargetBeanType() { + return this.descriptor.getDependencyType(); + } + + } + + + /** + * Interceptor that dereferences the target bean for the proxy by calling BeanFactory#getBean(String). + * + * @see EarlyBeanReferenceProxy#dereferenceTargetBean() + */ + private class ByNameLookupTargetBeanDereferencingInterceptor implements TargetBeanDereferencingInterceptor { + + private final Method beanMethod; + + public ByNameLookupTargetBeanDereferencingInterceptor(Method beanMethod) { + this.beanMethod = beanMethod; + } + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return beanFactory.getBean(BeanAnnotationHelper.determineBeanNameFor(beanMethod)); + } + + public Class getTargetBeanType() { + return beanMethod.getReturnType(); + } + + } + + + /** + * Interceptor that dereferences the target bean for the proxy and delegates the + * current method call to it. + * @see TargetBeanDereferencingInterceptor + */ + private static class TargetBeanDelegatingMethodInterceptor implements MethodInterceptor { + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + Object targetBean = ((EarlyBeanReferenceProxy)obj).dereferenceTargetBean(); + return method.invoke(targetBean, args); + } + + } + +} + diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java new file mode 100644 index 00000000000..dc54a0e94d7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/Feature.java @@ -0,0 +1,34 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Feature { + +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.java new file mode 100644 index 00000000000..20bb2802ba0 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotation.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Meta-annotation indicating that an annotation should be processed + * to produce a {@code FeatureSpecification}. + * + *

See {@link ComponentScan @ComponentScan} for an implementation example. + * + * @author Chris Beams + * @since 3.1 + * @see ComponentScan + * @see org.springframework.context.config.FeatureSpecification + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface FeatureAnnotation { + + /** + * Indicate the class that should be used to parse this annotation + * into a {@code FeatureSpecification}. + */ + Class parser(); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java new file mode 100644 index 00000000000..65a402de417 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureAnnotationParser.java @@ -0,0 +1,61 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Interface for parsing {@link AnnotationMetadata} from a {@link FeatureAnnotation} + * into a {@link FeatureSpecification} object. Used in conjunction with a + * {@link FeatureSpecificationExecutor} to provide a source-agnostic approach to + * handling configuration metadata. + * + *

For example, Spring's component-scanning can be configured via XML using + * the {@code context:component-scan} element or via the {@link ComponentScan} + * annotation. In either case, the metadata is the same -- only the source + * format differs. {@link ComponentScanBeanDefinitionParser} is used to create + * a specification from the {@code } XML element, while + * {@link ComponentScanAnnotationParser} creates a specification from the + * the annotation style. They both produce a {@link ComponentScanSpec} + * object that is ultimately delegated to a {@link ComponentScanExecutor} + * which understands how to configure a {@link ClassPathBeanDefinitionScanner}, + * perform actual scanning, and register actual bean definitions against the + * container. + * + *

Implementations must be instantiable via a no-arg constructor. + * + * TODO SPR-7420: documentation (clean up) + * TODO SPR-7420: rework so annotations declare their creator. + * + * + * @author Chris Beams + * @since 3.1 + * @see FeatureAnnotation#parser() + * @see FeatureSpecification + * @see FeatureSpecificationExecutor + */ +public interface FeatureAnnotationParser { + + /** + * Parse the given annotation metadata and populate a {@link FeatureSpecification} + * object suitable for execution by a {@link FeatureSpecificationExecutor}. + */ + FeatureSpecification process(AnnotationMetadata metadata); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.java new file mode 100644 index 00000000000..6fbfbf2b98b --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureConfiguration.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.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.stereotype.Component; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + * @see Configuration + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface FeatureConfiguration { + + /** + * Explicitly specify the name of the Spring bean definition associated + * with this FeatureConfiguration class. If left unspecified (the common case), + * a bean name will be automatically generated. + * + *

The custom name applies only if the FeatureConfiguration class is picked up via + * component scanning or supplied directly to a {@link AnnotationConfigApplicationContext}. + * If the FeatureConfiguration class is registered as a traditional XML bean definition, + * the name/id of the bean element will take precedence. + * + * @return the specified bean name, if any + * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator + */ + String value() default ""; +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java new file mode 100644 index 00000000000..e5aa6f54365 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/FeatureMethodExecutionException.java @@ -0,0 +1,28 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +@SuppressWarnings("serial") +class FeatureMethodExecutionException extends RuntimeException { + public FeatureMethodExecutionException(Throwable cause) { + super(cause); + } + + public FeatureMethodExecutionException(String message) { + super(message); + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.java new file mode 100644 index 00000000000..f85f311f83e --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/ProxyCreationException.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +@SuppressWarnings("serial") +class ProxyCreationException extends RuntimeException { + + public ProxyCreationException(String message) { + super(message); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java b/org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java new file mode 100644 index 00000000000..4c5e47e0c6f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/annotation/SimpleComponentRegistrar.java @@ -0,0 +1,46 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +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.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + +public class SimpleComponentRegistrar implements ComponentRegistrar { + + private final BeanDefinitionRegistry registry; + + public SimpleComponentRegistrar(BeanDefinitionRegistry registry) { + this.registry = registry; + } + + public String registerWithGeneratedName(BeanDefinition beanDefinition) { + return BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, this.registry); + } + + public void registerBeanComponent(BeanComponentDefinition component) { + BeanDefinitionReaderUtils.registerBeanDefinition(component, this.registry); + registerComponent(component); + } + + public void registerComponent(ComponentDefinition component) { + // no-op + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java new file mode 100644 index 00000000000..ac0a8a80f64 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractFeatureSpecification.java @@ -0,0 +1,77 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.parsing.SimpleProblemCollector; + + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public abstract class AbstractFeatureSpecification implements SourceAwareSpecification { + + private static final Object DUMMY_SOURCE = new Object(); + private static final String DUMMY_SOURCE_NAME = "dummySource"; + + protected Class executorType; + + private Object source = DUMMY_SOURCE; + private String sourceName = DUMMY_SOURCE_NAME; + + protected AbstractFeatureSpecification(Class executorType) { + this.executorType = executorType; + } + + public final boolean validate(ProblemReporter problemReporter) { + SimpleProblemCollector collector = new SimpleProblemCollector(this.source()); + this.doValidate(collector); + collector.reportProblems(problemReporter); + return collector.hasErrors() ? false : true; + } + + protected abstract void doValidate(SimpleProblemCollector reporter); + + public AbstractFeatureSpecification source(Object source) { + this.source = source; + return this; + } + + public Object source() { + return this.source; + } + + public AbstractFeatureSpecification sourceName(String sourceName) { + this.sourceName = sourceName; + return this; + } + + public String sourceName() { + return this.sourceName; + } + + public void execute(ExecutorContext executorContext) { + FeatureSpecificationExecutor executor = + BeanUtils.instantiateClass(this.executorType, FeatureSpecificationExecutor.class); + executor.execute(this, executorContext); + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.java b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.java new file mode 100644 index 00000000000..34d7bc08980 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AbstractSpecificationExecutor.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.util.Assert; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public abstract class AbstractSpecificationExecutor implements FeatureSpecificationExecutor { + + /** + * {@inheritDoc} + *

This implementation {@linkplain FeatureSpecification#validate() validates} the + * given specification and delegates it to {@link #doExecute(FeatureSpecification)} + * only if valid. + */ + @SuppressWarnings("unchecked") + public final void execute(FeatureSpecification spec, ExecutorContext executorContext) { + Assert.notNull(spec, "Specification must not be null"); + Assert.notNull(spec, "ExecutorContext must not be null"); + Class typeArg = GenericTypeResolver.resolveTypeArgument(this.getClass(), AbstractSpecificationExecutor.class); + Assert.isTrue(typeArg.equals(spec.getClass()), "Specification cannot be executed by this executor"); + if (spec.validate(executorContext.getProblemReporter())) { + doExecute((S)spec, executorContext); + } + } + + /** + * Execute the given specification, usually resulting in registration of bean definitions + * against a bean factory. + * @param specification the {@linkplain FeatureSpecification#validate() validated} specification + */ + protected abstract void doExecute(S specification, ExecutorContext executorContext); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java b/org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java new file mode 100644 index 00000000000..57d9516a68a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/AdviceMode.java @@ -0,0 +1,28 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public enum AdviceMode { + PROXY, + ASPECTJ +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java b/org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java new file mode 100644 index 00000000000..7c6a3a26dd7 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/ExecutorContext.java @@ -0,0 +1,81 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.env.Environment; +import org.springframework.core.io.ResourceLoader; + +/** + * TODO: rename to SpecificationContext? + * + * @author Chris Beams + * @since 3.1 + */ +public class ExecutorContext { + + private BeanDefinitionRegistry registry; + private ComponentRegistrar registrar; + private ResourceLoader resourceLoader; + private Environment environment; + private ProblemReporter problemReporter; + + public ExecutorContext() { } + + public void setRegistry(BeanDefinitionRegistry registry) { + this.registry = registry; + } + + public BeanDefinitionRegistry getRegistry() { + return this.registry; + } + + public void setRegistrar(ComponentRegistrar registrar) { + this.registrar = registrar; + } + + public ComponentRegistrar getRegistrar() { + return this.registrar; + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + public Environment getEnvironment() { + return this.environment; + } + + public void setProblemReporter(ProblemReporter problemReporter) { + this.problemReporter = problemReporter; + } + + public ProblemReporter getProblemReporter() { + return this.problemReporter; + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java new file mode 100644 index 00000000000..7cca49ee45a --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecification.java @@ -0,0 +1,127 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +import org.springframework.beans.factory.parsing.ProblemReporter; + +/** + * Interface to be implemented by objects that specify the configuration of a particular feature + * of the Spring container e.g., component-scanning, JMX MBean exporting, AspectJ auto-proxying, + * annotation-driven transaction management, and so on. + * + *

Many features of the Spring container can be configured using either XML or annotations. + * As one example, Spring's component scanning feature may be configured using + * either the {@code } XML element or (as of Spring 3.1) the + * {@code @ComponentScan} annotation. These two options are equivalent to one another, and users may + * choose between them as a matter of convention or preference. Fundamentally, both are declarative + * mechanisms for specifying how the Spring container should be configured. A {@code + * FeatureSpecification} object, then, is a way of representing this configuration information independent + * of its original source format be it XML, annotations, or otherwise. + * + *

A {@code FeatureSpecification} is responsible for {@linkplain #validate validating itself}. + * For example, a component-scanning specification would check that at least one base package has + * been specified, and otherwise register a {@code Problem} with a {@link ProblemReporter}. Taking + * this approach as opposed to throwing exceptions allows for maximum tooling and error reporting + * flexibility. + * + *

A {@link FeatureSpecificationExecutor} is used to carry out the instructions within a populated + * {@code FeatureSpecification}; this is where the "real work" happens. In the case of component scanning + * as above, it is within a {@code FeatureSpecificationExecutor} that a bean definition scanner is created, + * configured and invoked against the base packages specified. + * + *

{@code FeatureSpecification} objects may be populated and executed by Spring XML namespace element + * parsers on order to separate the concerns of XML parsing from Spring bean definition creation and + * registration. This type of use is mostly a framework-internal matter. More interesting to end users is + * the use of {@code FeatureSpecification} objects within {@code @FeatureConfiguration} classes and their + * {@code @Feature} methods. This functionality is new in Spring 3.1 and is the logical evolution of Spring's + * Java-based configuration support first released in Spring 3.0 (see {@code @Configuration} classes and + * {@code @Bean} methods). The primary goal of {@code Feature}-related support is to round out this + * Java-based support and allow users to configure all aspects of the Spring-container without requiring + * the use of XML. See "design notes" below for an example of this kind of use. + * + *

Notes on designing {@code FeatureSpecification} implementations

+ * + *

The public API of a {@code FeatureSpecification} should be designed for maximum ease of use + * within {@code @Feature} methods. Traditional JavaBean-style getters and setters should be dropped + * for a more fluent style that allows for method chaining. Consider the following example of a + * {@code @Feature} method: + * + *

+ * @Feature
+ * public TxAnnotationDriven tx(PlatformTransactionManager txManager) {
+ *    return new TxAnnotationDriven(txManager).proxyTargetClass(true);
+ * }
+ * 
+ * + * Notice how the creation and configuration of the {@code TxAnnotationDriven} specification is + * concise and reads well. This is facilitated by mutator methods that always return the + * specification object's 'this' reference, allowing for method chaining. A secondary design goal + * of this approach is that the resulting code tends to mirror corresponding XML namespace + * declarations, which most Spring users are already familiar with. For example, compare the + * code above with its XML counterpart: + * + *

{@code } + * + *

Typically, a user will call only the constructor and 'mutator' methods of a specification + * object. The accessor/getter methods, however, are typically called only by the specification's + * {@linkplain FeatureSpecificationExecutor executor} for the purpose of populating and registering + * bean definitions with the Spring container.For this reason, it is recommended that accessor + * methods be given package-private visibility. This creates a better experience for users from + * an IDE content-assist point of view as they will see only the public mutator methods, reducing + * any possible confusion. + * + *

Implementations should take care to allow for use of string-based bean names, placeholder + * ("${...}") and SpEL ("#{...}) expressions wherever they may be useful. + * While it is generally desirable to refer to dependent beans in pure Java, in certain cases a + * user may wish or need to refer by bean name. For example, the {@code TxAnnotationDriven} specification + * referenced above allows for specifying its transaction-manager reference by {@code String} or by + * {@code PlatformTransactionManager} reference. Such strings should always be candidates for placeholder + * replacement and SpEL evaluation for maximum configurability as well as parity with the featureset + * available in Spring XML. With regard to SpEL expressions, users should assume that only expressions + * evaluating to a bean name will be supported. While it is technically possible with SpEL to resolve + * a bean instance, specification executors will not support such use unless explicitly indicated. + * + *

See the Javadoc for {@code @FeatureConfiguration} classes and {@code @Feature} methods for + * information on their lifecycle and semantics. + * + * @author Chris Beams + * @since 3.1 + * @see FeatureSpecificationExecutor + * @see AbstractSpecificationExecutor + * @see org.springframework.context.annotation.Feature + * @see org.springframework.context.annotation.FeatureConfiguration + */ +public interface FeatureSpecification { + + /** + * Validate this specification instance to ensure all required properties + * have been set, including checks on mutually exclusive or mutually + * dependent properties. May in some cases modify the state of the + * specification e.g., instantiating types specified as strings. + * @see AbstractSpecificationExecutor#execute(Specification) + * @return whether any problems occurred during validation + */ + boolean validate(ProblemReporter problemReporter); + + /** + * Execute this specification instance, carrying out the instructions + * specified within. Should work by delegating to an underlying + * {@link FeatureSpecificationExecutor} for proper separation of concerns. + */ + void execute(ExecutorContext executorContext); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java new file mode 100644 index 00000000000..42f1c0bdaa5 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/FeatureSpecificationExecutor.java @@ -0,0 +1,41 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +/** + * Interface for executing a populated {@link FeatureSpecification}. Provides + * a generic mechanism for handling container configuration metadata regardless of + * origin in XML, annotations, or other source format. + * + * TODO SPR-7420: document (clean up) + * + * @author Chris Beams + * @since 3.1 + * @see AbstractSpecificationExecutor + * @see org.springframework.beans.factory.xml.BeanDefinitionParser + * @see org.springframework.context.annotation.FeatureAnnotationParser + * @see org.springframework.context.annotation.ComponentScanExecutor + */ +public interface FeatureSpecificationExecutor { + + /** + * Execute the given specification, usually resulting in registration + * of bean definitions against a bean factory. + */ + void execute(FeatureSpecification spec, ExecutorContext executorContext); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java b/org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java new file mode 100644 index 00000000000..82b6ea09cdd --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/context/config/SourceAwareSpecification.java @@ -0,0 +1,35 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public interface SourceAwareSpecification extends FeatureSpecification { + + String sourceName(); + + SourceAwareSpecification sourceName(String sourceName); + + Object source(); + + SourceAwareSpecification source(Object source); + +} diff --git a/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java index 4afbb314e8f..a4fb136b2b1 100644 --- a/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java +++ b/org.springframework.context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -22,16 +22,30 @@ 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; /** - * A factory for a ConversionService that installs default converters appropriate for most environments. - * Set the {@link #setConverters "converters"} property to supplement or override the default converters. + * 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 - * @see ConversionServiceFactory#createDefaultConversionService() */ public class ConversionServiceFactoryBean implements FactoryBean, InitializingBean { @@ -52,17 +66,17 @@ public class ConversionServiceFactoryBean implements FactoryBeanCreates a simple {@link GenericConversionService} instance by default. - * Subclasses may override to customize the ConversionService instance that gets created. + * Subclasses may override to customize the ConversionService instance that + * gets created. */ protected GenericConversionService createConversionService() { - return new GenericConversionService(); + return new DefaultConversionService(); } diff --git a/org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java new file mode 100644 index 00000000000..47a410b7fbe --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -0,0 +1,135 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.format.support; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; +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. + * + * @author Chris Beams + * @since 3.1 + */ +public class DefaultFormattingConversionService extends FormattingConversionService { + + private static final boolean jodaTimePresent = ClassUtils.isPresent( + "org.joda.time.LocalDate", DefaultFormattingConversionService.class.getClassLoader()); + + /** + * 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(StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) { + this.setEmbeddedValueResolver(embeddedValueResolver); + DefaultConversionService.addDefaultConverters(this); + if (registerDefaultFormatters) { + addDefaultFormatters(this); + } + } + + /** + * Add formatters appropriate for most environments, including number formatters and a Joda-Time + * date formatter if Joda-Time is present on the classpath. + * @param formatterRegistry the service to register default formatters against + */ + public static void addDefaultFormatters(FormatterRegistry formatterRegistry) { + formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + if (jodaTimePresent) { + new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry); + } else { + formatterRegistry.addFormatterForFieldAnnotation(new NoJodaDateTimeFormatAnnotationFormatterFactory()); + } + } + + + /** + * Dummy AnnotationFormatterFactory that simply fails if @DateTimeFormat is being used + * without the JodaTime library being present. + */ + private static final class NoJodaDateTimeFormatAnnotationFormatterFactory + implements AnnotationFormatterFactory { + + private final Set> fieldTypes; + + public NoJodaDateTimeFormatAnnotationFormatterFactory() { + Set> rawFieldTypes = new HashSet>(4); + rawFieldTypes.add(Date.class); + rawFieldTypes.add(Calendar.class); + rawFieldTypes.add(Long.class); + this.fieldTypes = Collections.unmodifiableSet(rawFieldTypes); + } + + public Set> getFieldTypes() { + return this.fieldTypes; + } + + public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { + throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); + } + + public Parser getParser(DateTimeFormat annotation, Class fieldType) { + throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); + } + } + +} diff --git a/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java index d1469f116a0..278d86830ad 100644 --- a/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java +++ b/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java @@ -16,10 +16,6 @@ package org.springframework.format.support; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; import java.util.Set; import org.springframework.beans.factory.FactoryBean; @@ -32,48 +28,51 @@ 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; -import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; -import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; -import org.springframework.util.ClassUtils; import org.springframework.util.StringValueResolver; /** - *

A factory for a {@link FormattingConversionService} that installs default - * converters and formatters for common types such as numbers and datetimes. - * - *

Converters and formatters can be registered declaratively through + * 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 + * 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 - * JodaTimeFormatterRegistrar, which registers a number of + * + *

A good example for registering converters and formatters in code is + * JodaTimeFormatterRegistrar, which registers a number of * date-related formatters and converters. For a more detailed list of cases - * see {@link #setFormatterRegistrars(Set)} - * + * 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 { - private static final boolean jodaTimePresent = ClassUtils.isPresent( - "org.joda.time.LocalDate", FormattingConversionService.class.getClassLoader()); - private Set converters; private Set formatters; private Set formatterRegistrars; - private StringValueResolver embeddedValueResolver; - private FormattingConversionService conversionService; + private StringValueResolver embeddedValueResolver; + private boolean registerDefaultFormatters = true; /** @@ -129,12 +128,12 @@ public class FormattingConversionServiceFactoryBean this.registerDefaultFormatters = registerDefaultFormatters; } + + // implementing InitializingBean + public void afterPropertiesSet() { - this.conversionService = new FormattingConversionService(); - this.conversionService.setEmbeddedValueResolver(this.embeddedValueResolver); - ConversionServiceFactory.addDefaultConverters(this.conversionService); + this.conversionService = new DefaultFormattingConversionService(this.embeddedValueResolver, this.registerDefaultFormatters); ConversionServiceFactory.registerConverters(this.converters, this.conversionService); - addDefaultFormatters(); registerFormatters(); } @@ -162,24 +161,14 @@ public class FormattingConversionServiceFactoryBean * through FormatterRegistrars. * @see #setFormatters(Set) * @see #setFormatterRegistrars(Set) + * @deprecated since Spring 3.1 in favor of {@link #setFormatterRegistrars(Set)} */ + @Deprecated protected void installFormatters(FormatterRegistry registry) { } // private helper methods - private void addDefaultFormatters() { - if (registerDefaultFormatters) { - this.conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); - if (jodaTimePresent) { - new JodaTimeFormatterRegistrar().registerFormatters(this.conversionService); - } else { - this.conversionService - .addFormatterForFieldAnnotation(new NoJodaDateTimeFormatAnnotationFormatterFactory()); - } - } - } - private void registerFormatters() { if (this.formatters != null) { for (Object formatter : this.formatters) { @@ -201,34 +190,4 @@ public class FormattingConversionServiceFactoryBean installFormatters(this.conversionService); } - /** - * Dummy AnnotationFormatterFactory that simply fails if @DateTimeFormat is being used - * without the JodaTime library being present. - */ - private static final class NoJodaDateTimeFormatAnnotationFormatterFactory - implements AnnotationFormatterFactory { - - private final Set> fieldTypes; - - public NoJodaDateTimeFormatAnnotationFormatterFactory() { - Set> rawFieldTypes = new HashSet>(4); - rawFieldTypes.add(Date.class); - rawFieldTypes.add(Calendar.class); - rawFieldTypes.add(Long.class); - this.fieldTypes = Collections.unmodifiableSet(rawFieldTypes); - } - - public Set> getFieldTypes() { - return this.fieldTypes; - } - - public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { - throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); - } - - public Parser getParser(DateTimeFormat annotation, Class fieldType) { - throw new IllegalStateException("JodaTime library not available - @DateTimeFormat not supported"); - } - } - } diff --git a/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index 2458de6f845..28c6bc6a62e 100644 --- a/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/org.springframework.context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * 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. diff --git a/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd b/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd index ad93b101270..59903b4e05a 100644 --- a/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd +++ b/org.springframework.context/src/main/resources/org/springframework/context/config/spring-context-3.1.xsd @@ -191,6 +191,7 @@ diff --git a/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java b/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java index d31815f1eb2..ef2b6094b31 100644 --- a/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java +++ b/org.springframework.context/src/test/java/example/scannable/FooServiceImpl.java @@ -65,6 +65,7 @@ public class FooServiceImpl implements FooService { private boolean initCalled = false; + @SuppressWarnings("unused") @PostConstruct private void init() { if (this.initCalled) { diff --git a/org.springframework.context/src/test/java/example/scannable/_package.java b/org.springframework.context/src/test/java/example/scannable/_package.java new file mode 100644 index 00000000000..4be3b9447bc --- /dev/null +++ b/org.springframework.context/src/test/java/example/scannable/_package.java @@ -0,0 +1,25 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.scannable; + + +/** + * Marker class for example.scannable package. + * + * @see org.springframework.context.annotation.ComponentScan#basePackageClasses() + */ +public class _package { } diff --git a/org.springframework.context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java b/org.springframework.context/src/test/java/example/scannable_scoped/CustomScopeAnnotationBean.java new file mode 100644 index 00000000000..b34d50318f8 --- /dev/null +++ b/org.springframework.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.scannable_scoped; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.stereotype.Component; + +@Component +@MyScope(BeanDefinition.SCOPE_PROTOTYPE) +public class CustomScopeAnnotationBean { +} diff --git a/org.springframework.context/src/test/java/example/scannable_scoped/MyScope.java b/org.springframework.context/src/test/java/example/scannable_scoped/MyScope.java new file mode 100644 index 00000000000..512740b90b4 --- /dev/null +++ b/org.springframework.context/src/test/java/example/scannable_scoped/MyScope.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.scannable_scoped; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ScopedProxyMode; + +public @interface MyScope { + String value() default BeanDefinition.SCOPE_SINGLETON; + ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT; +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java new file mode 100644 index 00000000000..42f1c9b2a27 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/BeanFactoryAwareFeatureConfigurationTests.java @@ -0,0 +1,67 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +/** + * Tests that @FeatureConfiguration classes may implement Aware interfaces, + * such as BeanFactoryAware. This is not generally recommended but occasionally + * useful, particularly in testing. + * + * @author Chris Beams + * @since 3.1 + */ +public class BeanFactoryAwareFeatureConfigurationTests { + @Test + public void test() { + ConfigurableApplicationContext ctx = + new AnnotationConfigApplicationContext(FeatureConfig.class); + FeatureConfig fc = ctx.getBean(FeatureConfig.class); + assertThat(fc.featureMethodWasCalled, is(true)); + assertThat(fc.gotBeanFactoryInTime, is(true)); + assertThat(fc.beanFactory, is(ctx.getBeanFactory())); + } + + @FeatureConfiguration + static class FeatureConfig implements BeanFactoryAware { + + ConfigurableListableBeanFactory beanFactory; + boolean featureMethodWasCalled = false; + boolean gotBeanFactoryInTime = false; + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableListableBeanFactory)beanFactory; + } + + @Feature + public FeatureSpecification f() { + this.featureMethodWasCalled = true; + this.gotBeanFactoryInTime = (this.beanFactory != null); + return new StubSpecification(); + } + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java new file mode 100644 index 00000000000..a418258fe2d --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -0,0 +1,249 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; + +import java.io.IOException; +import java.util.HashSet; + +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.CustomAutowireConfigurer; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.SimpleMapScope; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.ComponentScanParserTests.CustomAnnotationAutowiredBean; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.SerializationTestUtils; + +import example.scannable.FooService; +import example.scannable.ScopedProxyTestBean; +import example.scannable_scoped.CustomScopeAnnotationBean; +import example.scannable_scoped.MyScope; + +/** + * Integration tests for processing ComponentScan-annotated Configuration + * classes. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanAnnotationIntegrationTests { + @Test + public void controlScan() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan(example.scannable._package.class.getPackage().getName()); + ctx.refresh(); + assertThat("control scan for example.scannable package failed to register FooServiceImpl bean", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @Test + public void viaContextRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanAnnotatedConfig.class); + ctx.refresh(); + ctx.getBean(ComponentScanAnnotatedConfig.class); + ctx.getBean(TestBean.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfig"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @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("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfig_WithValueAttribute"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @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("config class bean not found", ctx.containsBeanDefinition("componentScanAnnotatedConfig"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered " + + "as bean definition did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } + + @Test + public void invalidComponentScanDeclaration_noPackagesSpecified() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithNoPackagesConfig.class); + try { + ctx.refresh(); + fail("Expected exception when parsing @ComponentScan definition that declares no packages"); + } catch (BeanDefinitionParsingException ex) { + assertThat(ex.getMessage(), containsString("At least one base package must be specified")); + } + } + + @Test + public void withCustomBeanNameGenerator() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithBeanNameGenenerator.class); + ctx.refresh(); + assertThat(ctx.containsBean("custom_fooServiceImpl"), is(true)); + assertThat(ctx.containsBean("fooServiceImpl"), is(false)); + } + + @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), not(sameInstance(ctx.getBean(CustomScopeAnnotationBean.class)))); + } + + @Test + public void withCustomTypeFilter() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ComponentScanWithCustomTypeFilter.class); + CustomAnnotationAutowiredBean testBean = ctx.getBean(CustomAnnotationAutowiredBean.class); + assertThat(testBean.getDependency(), notNullValue()); + } + + @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), is(true)); + // test serializability + assertThat(bean.foo(1), equalTo("bar")); + FooService deserialized = (FooService) SerializationTestUtils.serializeAndDeserialize(bean); + assertThat(deserialized, notNullValue()); + assertThat(deserialized.foo(1), equalTo("bar")); + } + + @Test + public void withBasePackagesAndValueAlias() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanWithBasePackagesAndValueAlias.class); + ctx.refresh(); + assertThat(ctx.containsBean("fooServiceImpl"), is(true)); + } + +} + + +@Configuration +@ComponentScan(basePackageClasses=example.scannable._package.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 ComponentScanWithBeanNameGenenerator { } + +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 { } + +class MyScopeMetadataResolver extends AnnotationScopeMetadataResolver { + MyScopeMetadataResolver() { + this.scopeAnnotationType = MyScope.class; + } +} + +@Configuration +@ComponentScan(basePackages="org.springframework.context.annotation", + useDefaultFilters=false, + includeFilters=@Filter(type=FilterType.CUSTOM, value=ComponentScanParserTests.CustomTypeFilter.class), + // exclude this class from scanning since it's in the scanned package + excludeFilters=@Filter(type=FilterType.ASSIGNABLE_TYPE, value=ComponentScanWithCustomTypeFilter.class)) +class ComponentScanWithCustomTypeFilter { + @Bean + @SuppressWarnings({ "rawtypes", "serial", "unchecked" }) + public CustomAutowireConfigurer customAutowireConfigurer() { + CustomAutowireConfigurer cac = new CustomAutowireConfigurer(); + cac.setCustomQualifierTypes(new HashSet() {{ add(ComponentScanParserTests.CustomAnnotation.class); }}); + return cac; + } + + public ComponentScanParserTests.CustomAnnotationAutowiredBean testBean() { + return new ComponentScanParserTests.CustomAnnotationAutowiredBean(); + } +} + +@Configuration +@ComponentScan(basePackages="example.scannable", + scopedProxy=ScopedProxyMode.INTERFACES, + useDefaultFilters=false, + includeFilters=@Filter(type=FilterType.ASSIGNABLE_TYPE, value=ScopedProxyTestBean.class)) +class ComponentScanWithScopedProxy { } + +@Configuration +@ComponentScan( + value="example.scannable", + basePackages="example.scannable", + basePackageClasses=example.scannable._package.class) +class ComponentScanWithBasePackagesAndValueAlias { } diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java index 39f8dffc112..56045f669b6 100644 --- a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationTests.java @@ -18,8 +18,7 @@ package org.springframework.context.annotation; import org.junit.Test; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; -import org.springframework.context.annotation.ComponentScan.ExcludeFilter; -import org.springframework.context.annotation.ComponentScan.IncludeFilter; +import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.core.type.filter.TypeFilter; /** @@ -27,11 +26,13 @@ import org.springframework.core.type.filter.TypeFilter; * * @author Chris Beams * @since 3.1 + * @see ComponentScanAnnotationIntegrationTests */ public class ComponentScanAnnotationTests { @Test - public void test() { - + public void noop() { + // no-op; the @ComponentScan-annotated MyConfig class below simply excercises + // available attributes of the annotation. } } @@ -39,22 +40,22 @@ public class ComponentScanAnnotationTests { @Configuration @ComponentScan( - packageOf={TestBean.class}, + basePackageClasses={TestBean.class}, nameGenerator = DefaultBeanNameGenerator.class, scopedProxy = ScopedProxyMode.NO, scopeResolver = AnnotationScopeMetadataResolver.class, useDefaultFilters = false, resourcePattern = "**/*custom.class", includeFilters = { - @IncludeFilter(type = FilterType.ANNOTATION, value = MyAnnotation.class) + @Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class) }, excludeFilters = { - @ExcludeFilter(type = FilterType.CUSTOM, value = TypeFilter.class) + @Filter(type = FilterType.CUSTOM, value = TypeFilter.class) } ) class MyConfig { } -@ComponentScan(packageOf=example.scannable.NamedComponent.class) +@ComponentScan(basePackageClasses=example.scannable.NamedComponent.class) class SimpleConfig { } \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java new file mode 100644 index 00000000000..287c05a024f --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanExecutorTests.java @@ -0,0 +1,67 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.config.ExecutorContext; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.mock.env.MockEnvironment; + +/** + * Unit tests for {@link ComponentScanExecutor}. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanExecutorTests { + + private ComponentScanExecutor executor; + private ExecutorContext executorContext; + private DefaultListableBeanFactory bf; + + @Before + public void setUp() { + this.bf = new DefaultListableBeanFactory(); + this.executor = new ComponentScanExecutor(); + this.executorContext = new ExecutorContext(); + this.executorContext.setRegistry(bf); + this.executorContext.setResourceLoader(new DefaultResourceLoader()); + this.executorContext.setEnvironment(new MockEnvironment()); + this.executorContext.setRegistrar(new SimpleComponentRegistrar(bf)); + this.executorContext.setProblemReporter(new FailFastProblemReporter()); + } + + @Test + public void validSpec() { + this.executor.execute(new ComponentScanSpec("example.scannable"), this.executorContext); + assertThat(bf.containsBean("fooServiceImpl"), is(true)); + } + + @Test(expected=BeanDefinitionParsingException.class) + public void invalidSpec() { + // ff problem reporter should throw due to no packages specified + this.executor.execute(new ComponentScanSpec(), this.executorContext); + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java new file mode 100644 index 00000000000..8ddbd03240f --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanFeatureTests.java @@ -0,0 +1,55 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +public class ComponentScanFeatureTests { + @Test + public void viaContextRegistration() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(ComponentScanFeatureConfig.class); + ctx.register(ComponentScanFeatureConfig.Features.class); + ctx.refresh(); + ctx.getBean(ComponentScanFeatureConfig.class); + ctx.getBean(TestBean.class); + assertThat("config class bean not found", ctx.containsBeanDefinition("componentScanFeatureConfig"), is(true)); + assertThat("@ComponentScan annotated @Configuration class registered directly against " + + "AnnotationConfigApplicationContext did not trigger component scanning as expected", + ctx.containsBean("fooServiceImpl"), is(true)); + } +} + +@Configuration +//@Import(ComponentScanFeatureConfig.Features.class) +class ComponentScanFeatureConfig { + @FeatureConfiguration + static class Features { + @Feature + public ComponentScanSpec componentScan() { + return new ComponentScanSpec(example.scannable._package.class); + } + } + + @Bean + public TestBean testBean() { + return new TestBean(); + } +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java new file mode 100644 index 00000000000..cc9f1ecbb81 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/ComponentScanSpecTests.java @@ -0,0 +1,411 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.springframework.util.StringUtils.arrayToCommaDelimitedString; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.beans.factory.parsing.Problem; +import org.springframework.beans.factory.parsing.ProblemReporter; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultBeanNameGenerator; +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.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * Unit tests for {@link ComponentScanSpec}. + * + * @author Chris Beams + * @since 3.1 + */ +public class ComponentScanSpecTests { + + private CollatingProblemReporter problemReporter; + private ClassLoader classLoader; + + + @Before + public void setUp() { + problemReporter = new CollatingProblemReporter(); + classLoader = ClassUtils.getDefaultClassLoader(); + } + + @Test + public void includeAnnotationConfig() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.includeAnnotationConfig(), nullValue()); + spec.includeAnnotationConfig(true); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(true)); + spec.includeAnnotationConfig(false); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig("trUE"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(true)); + spec.includeAnnotationConfig("falSE"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig(""); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig((String)null); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), is(false)); + spec.includeAnnotationConfig((Boolean)null); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeAnnotationConfig(), nullValue()); + } + + @Test + public void resourcePattern() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.resourcePattern(), nullValue()); + assertThat(spec.validate(problemReporter), is(true)); + spec.resourcePattern("**/Foo*.class"); + assertThat(spec.resourcePattern(), is("**/Foo*.class")); + assertThat(spec.validate(problemReporter), is(true)); + spec.resourcePattern(""); + assertThat(spec.resourcePattern(), is("**/Foo*.class")); + assertThat(spec.validate(problemReporter), is(true)); + spec.resourcePattern(null); + assertThat(spec.resourcePattern(), is("**/Foo*.class")); + assertThat(spec.validate(problemReporter), is(true)); + } + + @Test + public void useDefaultFilters() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.useDefaultFilters(), nullValue()); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters((Boolean)null); + assertThat(spec.useDefaultFilters(), nullValue()); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters(true); + assertThat(spec.useDefaultFilters(), is(true)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters(false); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters("trUE"); + assertThat(spec.useDefaultFilters(), is(true)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters("falSE"); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters(""); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + spec.useDefaultFilters((String)null); + assertThat(spec.useDefaultFilters(), is(false)); + assertThat(spec.validate(problemReporter), is(true)); + } + + @Test + public void includeFilters() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.includeFilters( + new AnnotationTypeFilter(MyAnnotation.class), + new AssignableTypeFilter(Object.class)); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeFilters().length, is(2)); + } + + @Test + public void stringIncludeExcludeFilters() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.addIncludeFilter("annotation", MyAnnotation.class.getName(), classLoader); + spec.addExcludeFilter("assignable", Object.class.getName(), classLoader); + spec.addExcludeFilter("annotation", Override.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.includeFilters().length, is(1)); + assertThat(spec.excludeFilters().length, is(2)); + } + + @Test + public void bogusStringIncludeFilter() throws IOException { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.addIncludeFilter("bogus-type", "bogus-expr", classLoader); + assertThat(spec.validate(problemReporter), is(false)); + assertThat(spec.includeFilters().length, is(1)); + try { + spec.includeFilters()[0].match(null, null); + fail("expected placholder TypeFilter to throw exception"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @Test + public void exerciseFilterTypes() throws IOException { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.addIncludeFilter("aspectj", "*..Bogus", classLoader); + assertThat(spec.validate(problemReporter), is(true)); + spec.addIncludeFilter("regex", ".*Foo", classLoader); + assertThat(spec.validate(problemReporter), is(true)); + spec.addIncludeFilter("custom", StubTypeFilter.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + spec.addIncludeFilter("custom", "org.NonExistentTypeFilter", classLoader); + assertThat(spec.validate(problemReporter), is(false)); + spec.addIncludeFilter("custom", NonNoArgResolver.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void missingBasePackages() { + ComponentScanSpec spec = new ComponentScanSpec(); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withBasePackageViaAdderMethod() { + ComponentScanSpec spec = new ComponentScanSpec(); + spec.addBasePackage("org.p1"); + spec.addBasePackage("org.p2"); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "org.p1", "org.p2"); + } + + @Test + public void withBasePackagesViaStringConstructor() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1", "org.p2"); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "org.p1", "org.p2"); + } + + @Test + public void withBasePackagesViaClassConstructor() { + ComponentScanSpec spec = new ComponentScanSpec(java.lang.Object.class, java.io.Closeable.class); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "java.lang", "java.io"); + } + + @Test + public void forDelimitedPackages() { + ComponentScanSpec spec = ComponentScanSpec.forDelimitedPackages("pkg.one,pkg.two"); + assertTrue(ObjectUtils.containsElement(spec.basePackages(), "pkg.one")); + assertTrue(ObjectUtils.containsElement(spec.basePackages(), "pkg.two")); + assertThat(spec.basePackages().length, is(2)); + } + + @Test + public void withSomeEmptyBasePackages() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1", "", "org.p3"); + assertThat(spec.validate(problemReporter), is(true)); + assertExactContents(spec.basePackages(), "org.p1", "org.p3"); + } + + @Test + public void withAllEmptyBasePackages() { + ComponentScanSpec spec = new ComponentScanSpec("", "", ""); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withInstanceBeanNameGenerator() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.beanNameGenerator(), nullValue()); + BeanNameGenerator bng = new DefaultBeanNameGenerator(); + spec.beanNameGenerator(bng); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.beanNameGenerator(), is(bng)); + } + + @Test + public void withStringBeanNameGenerator() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.beanNameGenerator(DefaultBeanNameGenerator.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.beanNameGenerator(), instanceOf(DefaultBeanNameGenerator.class)); + } + + @Test + public void withInstanceScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + assertThat(spec.scopeMetadataResolver(), nullValue()); + ScopeMetadataResolver smr = new AnnotationScopeMetadataResolver(); + spec.scopeMetadataResolver(smr); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopeMetadataResolver(), is(smr)); + } + + @Test + public void withStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(AnnotationScopeMetadataResolver.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopeMetadataResolver(), instanceOf(AnnotationScopeMetadataResolver.class)); + } + + @Test + public void withNonAssignableStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(Object.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withNonExistentStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver("org.Bogus", classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withNonNoArgStringScopeMetadataResolver() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(NonNoArgResolver.class.getName(), classLoader); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void withStringScopedProxyMode() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopedProxyMode("targetCLASS"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopedProxyMode(), is(ScopedProxyMode.TARGET_CLASS)); + spec.scopedProxyMode("interFACES"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopedProxyMode(), is(ScopedProxyMode.INTERFACES)); + spec.scopedProxyMode("nO"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.scopedProxyMode(), is(ScopedProxyMode.NO)); + spec.scopedProxyMode("bogus"); + assertThat(spec.validate(problemReporter), is(false)); + assertThat(spec.scopedProxyMode(), nullValue()); + } + + @Test + public void withScopeMetadataResolverAndScopedProxyMode() { + ComponentScanSpec spec = new ComponentScanSpec("org.p1"); + spec.scopeMetadataResolver(new AnnotationScopeMetadataResolver()); + assertThat(spec.validate(problemReporter), is(true)); + spec.scopedProxyMode(ScopedProxyMode.INTERFACES); + assertThat(spec.validate(problemReporter), is(false)); + } + + @Test + public void addBasePackage() { + ComponentScanSpec spec = new ComponentScanSpec(); + spec.addBasePackage("foo.bar"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.basePackages().length, is(1)); + } + + @Test + public void addBasePackageWithConstructor() { + ComponentScanSpec spec = new ComponentScanSpec("my.pkg"); + spec.addBasePackage("foo.bar"); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.basePackages().length, is(2)); + } + + @Test + public void addExcludeFilterString() { + ComponentScanSpec spec = new ComponentScanSpec("my.pkg"); + spec.addExcludeFilter("annotation", MyAnnotation.class.getName(), ClassUtils.getDefaultClassLoader()); + assertThat(spec.validate(problemReporter), is(true)); + assertThat(spec.excludeFilters().length, is(1)); + assertThat(spec.excludeFilters()[0], instanceOf(AnnotationTypeFilter.class)); + } + + @Test(expected=BeanDefinitionParsingException.class) + public void withFailFastProblemReporter() { + new ComponentScanSpec().validate(new FailFastProblemReporter()); + } + + private void assertExactContents(T[] actual, T... expected) { + if (actual.length >= expected.length) { + for (int i = 0; i < expected.length; i++) { + assertThat( + String.format("element number %d in actual is incorrect. actual: [%s], expected: [%s]", + i, arrayToCommaDelimitedString(actual), arrayToCommaDelimitedString(expected)), + actual[i], equalTo(expected[i])); + } + } + assertThat(String.format("actual contains incorrect number of arguments. actual: [%s], expected: [%s]", + arrayToCommaDelimitedString(actual), arrayToCommaDelimitedString(expected)), + actual.length, equalTo(expected.length)); + } + + + private static class CollatingProblemReporter implements ProblemReporter { + + private List errors = new ArrayList(); + + private List warnings = new ArrayList(); + + + public void fatal(Problem problem) { + throw new BeanDefinitionParsingException(problem); + } + + public void error(Problem problem) { + this.errors.add(problem); + } + + @SuppressWarnings("unused") + public Problem[] getErrors() { + return this.errors.toArray(new Problem[this.errors.size()]); + } + + public void warning(Problem problem) { + System.out.println(problem); + this.warnings.add(problem); + } + + @SuppressWarnings("unused") + public Problem[] getWarnings() { + return this.warnings.toArray(new Problem[this.warnings.size()]); + } + } + + + private static class NonNoArgResolver implements ScopeMetadataResolver { + public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) { + throw new UnsupportedOperationException(); + } + } + + private static class StubTypeFilter implements TypeFilter { + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + throw new UnsupportedOperationException(); + } + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java new file mode 100644 index 00000000000..ae481bdae0d --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/EarlyBeanReferenceProxyCreatorTests.java @@ -0,0 +1,316 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.springframework.context.annotation.EarlyBeanReferenceProxyCreator.FINAL_CLASS_ERROR_MESSAGE; +import static org.springframework.context.annotation.EarlyBeanReferenceProxyCreator.MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE; +import static org.springframework.context.annotation.EarlyBeanReferenceProxyCreator.PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.MethodParameter; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Unit tests for {@link EarlyBeanReferenceProxyCreator}, ensuring that + * {@link EarlyBeanReferenceProxy} objects behave properly. + * + * @author Chris Beams + * @since 3.1 + */ +public class EarlyBeanReferenceProxyCreatorTests { + + private DefaultListableBeanFactory bf; + + @Before + public void setUp() { + bf = new DefaultListableBeanFactory(); + } + + @Test + public void proxyToStringAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy.toString(), equalTo("EarlyBeanReferenceProxy for bean of type TestBean")); + } + + @Test(expected=NoSuchBeanDefinitionException.class) + public void proxyThrowsNoSuchBeanDefinitionExceptionWhenDelegatingMethodCallToNonExistentBean() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + proxy.getName(); + } + + @Test + public void proxyHashCodeAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy.hashCode(), equalTo(System.identityHashCode(proxy))); + } + + @Test + public void proxyEqualsAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy.equals(new Object()), is(false)); + assertThat(proxy.equals(proxy), is(true)); + + TestBean proxy2 = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy, not(sameInstance(proxy2))); + assertThat(proxy.equals(proxy2), is(false)); + assertThat(proxy2.equals(proxy), is(false)); + assertThat(proxy2.equals(proxy2), is(true)); + } + + @Test + public void proxyFinalizeAvoidsEagerInstantiation() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + + BeanWithFinalizer proxy = (BeanWithFinalizer) pc.createProxy(descriptorFor(BeanWithFinalizer.class)); + + assertThat(BeanWithFinalizer.finalizerWasCalled, is(false)); + BeanWithFinalizer.class.getDeclaredMethod("finalize").invoke(proxy); + assertThat(BeanWithFinalizer.finalizerWasCalled, is(false)); + } + + @Test + public void proxyMethodsDelegateToTargetBeanCausingSingletonRegistrationIfNecessary() throws Exception { + bf.registerBeanDefinition("testBean", + BeanDefinitionBuilder.rootBeanDefinition(TestBean.class) + .addPropertyValue("name", "testBeanName").getBeanDefinition()); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(bf.containsBeanDefinition("testBean"), is(true)); + assertThat(bf.containsSingleton("testBean"), is(false)); + assertThat(proxy.getName(), equalTo("testBeanName")); + assertThat(bf.containsSingleton("testBean"), is(true)); + } + + @Test + public void beanAnnotatedMethodsReturnEarlyProxyAsWell() throws Exception { + bf.registerBeanDefinition("componentWithInterfaceBeanMethod", new RootBeanDefinition(ComponentWithInterfaceBeanMethod.class)); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithInterfaceBeanMethod proxy = (ComponentWithInterfaceBeanMethod) pc.createProxy(descriptorFor(ComponentWithInterfaceBeanMethod.class)); + + ITestBean bean = proxy.aBeanMethod(); + assertThat(bean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(bf.containsBeanDefinition("componentWithInterfaceBeanMethod"), is(true)); + assertThat("calling a @Bean method on an EarlyBeanReferenceProxy object " + + "should not cause its instantation/registration", + bf.containsSingleton("componentWithInterfaceBeanMethod"), is(false)); + + Object obj = proxy.normalInstanceMethod(); + assertThat(bf.containsSingleton("componentWithInterfaceBeanMethod"), is(true)); + assertThat(obj, not(instanceOf(EarlyBeanReferenceProxy.class))); + } + + @Test + public void proxiesReturnedFromBeanAnnotatedMethodsDereferenceAndDelegateToTheirTargetBean() throws Exception { + bf.registerBeanDefinition("componentWithConcreteBeanMethod", new RootBeanDefinition(ComponentWithConcreteBeanMethod.class)); + RootBeanDefinition beanMethodBeanDef = new RootBeanDefinition(); + beanMethodBeanDef.setFactoryBeanName("componentWithConcreteBeanMethod"); + beanMethodBeanDef.setFactoryMethodName("aBeanMethod"); + bf.registerBeanDefinition("aBeanMethod", beanMethodBeanDef); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithConcreteBeanMethod proxy = (ComponentWithConcreteBeanMethod) pc.createProxy(descriptorFor(ComponentWithConcreteBeanMethod.class)); + + TestBean bean = proxy.aBeanMethod(); + assertThat(bean.getName(), equalTo("concrete")); + } + + @Test + public void interfaceBeansAreProxied() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ITestBean proxy = (ITestBean) pc.createProxy(descriptorFor(ITestBean.class)); + + assertThat(proxy, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(proxy.getClass()), is(true)); + assertEquals( + "interface-based bean proxies should have Object as superclass", + proxy.getClass().getSuperclass(), Object.class); + } + + @Test + public void concreteBeansAreProxied() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + TestBean proxy = (TestBean) pc.createProxy(descriptorFor(TestBean.class)); + + assertThat(proxy, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(proxy.getClass()), is(true)); + assertEquals( + "concrete bean proxies should have the bean class as superclass", + proxy.getClass().getSuperclass(), TestBean.class); + } + + @Test + public void beanAnnotatedMethodsWithInterfaceReturnTypeAreProxied() throws Exception { + bf.registerBeanDefinition("componentWithInterfaceBeanMethod", new RootBeanDefinition(ComponentWithInterfaceBeanMethod.class)); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithInterfaceBeanMethod proxy = (ComponentWithInterfaceBeanMethod) pc.createProxy(descriptorFor(ComponentWithInterfaceBeanMethod.class)); + + ITestBean bean = proxy.aBeanMethod(); + assertThat(bean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(bean.getClass()), is(true)); + assertEquals( + "interface-based bean proxies should have Object as superclass", + bean.getClass().getSuperclass(), Object.class); + } + + @Test + public void beanAnnotatedMethodsWithConcreteReturnTypeAreProxied() throws Exception { + bf.registerBeanDefinition("componentWithConcreteBeanMethod", new RootBeanDefinition(ComponentWithConcreteBeanMethod.class)); + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + ComponentWithConcreteBeanMethod proxy = (ComponentWithConcreteBeanMethod) pc.createProxy(descriptorFor(ComponentWithConcreteBeanMethod.class)); + + TestBean bean = proxy.aBeanMethod(); + assertThat(bean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(AopUtils.isCglibProxyClass(bean.getClass()), is(true)); + assertEquals( + "concrete bean proxies should have the bean class as superclass", + bean.getClass().getSuperclass(), TestBean.class); + } + + @Test + public void attemptToProxyClassMissingNoArgConstructorFailsGracefully() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + try { + pc.createProxy(descriptorFor(BeanMissingNoArgConstructor.class)); + fail("expected ProxyCreationException"); + } catch(ProxyCreationException ex) { + assertThat(ex.getMessage(), + equalTo(format(MISSING_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, BeanMissingNoArgConstructor.class.getName()))); + } + } + + @Test + public void attemptToProxyClassWithPrivateNoArgConstructorFailsGracefully() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + try { + pc.createProxy(descriptorFor(BeanWithPrivateNoArgConstructor.class)); + fail("expected ProxyCreationException"); + } catch(ProxyCreationException ex) { + assertThat(ex.getMessage(), + equalTo(format(PRIVATE_NO_ARG_CONSTRUCTOR_ERROR_MESSAGE, BeanWithPrivateNoArgConstructor.class.getName()))); + } + } + + @Test + public void attemptToProxyFinalClassFailsGracefully() throws Exception { + EarlyBeanReferenceProxyCreator pc = new EarlyBeanReferenceProxyCreator(bf); + try { + pc.createProxy(descriptorFor(FinalBean.class)); + fail("expected ProxyCreationException"); + } catch(ProxyCreationException ex) { + assertThat(ex.getMessage(), + equalTo(format(FINAL_CLASS_ERROR_MESSAGE, FinalBean.class.getName()))); + } + } + + private DependencyDescriptor descriptorFor(Class paramType) throws Exception { + @SuppressWarnings("unused") + class C { + void m(ITestBean p) { } + void m(TestBean p) { } + void m(BeanMissingNoArgConstructor p) { } + void m(BeanWithPrivateNoArgConstructor p) { } + void m(FinalBean p) { } + void m(BeanWithFinalizer p) { } + void m(ComponentWithConcreteBeanMethod p) { } + void m(ComponentWithInterfaceBeanMethod p) { } + } + + Method targetMethod = C.class.getDeclaredMethod("m", new Class[] { paramType }); + MethodParameter mp = new MethodParameter(targetMethod, 0); + DependencyDescriptor dd = new DependencyDescriptor(mp, true, false); + return dd; + } + + + static class BeanMissingNoArgConstructor { + BeanMissingNoArgConstructor(Object o) { } + } + + + static class BeanWithPrivateNoArgConstructor { + private BeanWithPrivateNoArgConstructor() { } + } + + + static final class FinalBean { + } + + + static class BeanWithFinalizer { + static Boolean finalizerWasCalled = false; + + @Override + protected void finalize() throws Throwable { + finalizerWasCalled = true; + } + } + + + static class ComponentWithConcreteBeanMethod { + @Bean + public TestBean aBeanMethod() { + return new TestBean("concrete"); + } + + public Object normalInstanceMethod() { + return new Object(); + } + } + + + static class ComponentWithInterfaceBeanMethod { + @Bean + public ITestBean aBeanMethod() { + return new TestBean("interface"); + } + + public Object normalInstanceMethod() { + return new Object(); + } + } +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java new file mode 100644 index 00000000000..a7831d0eaa3 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationClassTests.java @@ -0,0 +1,52 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import org.junit.Test; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +public class FeatureConfigurationClassTests { + + @Test(expected=FeatureMethodExecutionException.class) + public void featureConfigurationClassesMustNotContainBeanAnnotatedMethods() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(FeatureConfigWithBeanAnnotatedMethod.class); + ctx.refresh(); + } + +} + + +@FeatureConfiguration +class FeatureConfigWithBeanAnnotatedMethod { + /** + * This is illegal use. @FeatureConfiguration classes cannot have @Bean methods. + */ + @Bean + public TestBean testBean() { + return new TestBean(); + } + + /** + * This will never get called. An exception will first be raised regarding the illegal @Bean method above. + */ + @Feature + public FeatureSpecification feature() { + return new StubSpecification(); + } +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml new file mode 100644 index 00000000000..930cadfa379 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java new file mode 100644 index 00000000000..466e65b14e3 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportResourceTests.java @@ -0,0 +1,69 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.TestBean; + +/** + * Tests proving that @FeatureConfiguration classes may be use @ImportResource + * and then parameter autowire beans declared in the imported resource(s). + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureConfigurationImportResourceTests { + + @Test + public void importResourceFromFeatureConfiguration() { + ApplicationContext ctx = + new AnnotationConfigApplicationContext(ImportingFeatureConfig.class); + TestBean testBean = ctx.getBean(TestBean.class); + assertThat(testBean.getName(), equalTo("beanFromXml")); + + // and just quickly prove that the target of the bean proxied for the Feature method + // is indeed the same singleton instance as the one we just pulled from the container + ImportingFeatureConfig ifc = ctx.getBean(ImportingFeatureConfig.class); + TestBean proxyBean = ifc.testBean; + assertThat(proxyBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertNotSame(proxyBean, testBean); + assertSame(((EarlyBeanReferenceProxy)proxyBean).dereferenceTargetBean(), testBean); + } + + @FeatureConfiguration + @ImportResource("org/springframework/context/annotation/FeatureConfigurationImportResourceTests-context.xml") + static class ImportingFeatureConfig { + TestBean testBean; + + @Feature + public FeatureSpecification f(TestBean testBean) { + this.testBean = testBean; + return new StubSpecification(); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java new file mode 100644 index 00000000000..53141541225 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureConfigurationImportTests.java @@ -0,0 +1,105 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +/** + * Tests proving that @Configuration classes may @Import @FeatureConfiguration + * classes, and vice versa. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureConfigurationImportTests { + + @Test + public void importFeatureConfigurationFromConfiguration() { + ConfigurableApplicationContext ctx = + new AnnotationConfigApplicationContext(ImportingConfig.class); + ImportedFeatureConfig ifc = ctx.getBean(ImportedFeatureConfig.class); + assertThat( + "@FeatureConfiguration class was imported and registered " + + "as a bean but its @Feature method was never called", + ifc.featureMethodWasCalled, is(true)); + } + + @Test + public void importConfigurationFromFeatureConfiguration() { + ConfigurableApplicationContext ctx = + new AnnotationConfigApplicationContext(ImportingFeatureConfig.class); + ImportingFeatureConfig ifc = ctx.getBean(ImportingFeatureConfig.class); + ImportedConfig ic = ctx.getBean(ImportedConfig.class); + assertThat( + "@FeatureConfiguration class was registered directly against " + + "the container but its @Feature method was never called", + ifc.featureMethodWasCalled, is(true)); + assertThat( + "@Configuration class was @Imported but its @Bean method" + + "was never registered / called", + ic.beanMethodWasCalled, is(true)); + } + + + @Configuration + @Import(ImportedFeatureConfig.class) + static class ImportingConfig { + } + + + @FeatureConfiguration + static class ImportedFeatureConfig { + boolean featureMethodWasCalled = false; + + @Feature + public FeatureSpecification f() { + this.featureMethodWasCalled = true; + return new StubSpecification(); + } + } + + + @Configuration + static class ImportedConfig { + boolean beanMethodWasCalled = true; + @Bean + public TestBean testBean() { + this.beanMethodWasCalled = true; + return new TestBean(); + } + } + + + @FeatureConfiguration + @Import(ImportedConfig.class) + static class ImportingFeatureConfig { + boolean featureMethodWasCalled = false; + + @Feature + public FeatureSpecification f() { + this.featureMethodWasCalled = true; + return new StubSpecification(); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java new file mode 100644 index 00000000000..bab82187afb --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodBeanReferenceTests.java @@ -0,0 +1,76 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Tests proving that @Feature methods may reference the product of @Bean methods. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureMethodBeanReferenceTests { + + @Test + public void test() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(FeatureConfig.class, Config.class); + ctx.refresh(); + + TestBean registeredBean = ctx.getBean("testBean", TestBean.class); + TestBean proxiedBean = ctx.getBean(FeatureConfig.class).testBean; + + assertThat(registeredBean, not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(proxiedBean, notNullValue()); + assertThat(proxiedBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(proxiedBean.getSpouse(), is(registeredBean.getSpouse())); + } + + + @FeatureConfiguration + static class FeatureConfig { + TestBean testBean; + + @Feature + public FeatureSpecification f(TestBean testBean) { + this.testBean = testBean; + return new StubSpecification(); + } + } + + + @Configuration + static class Config { + @Bean + public ITestBean testBean() { + return new TestBean(new TestBean("mySpouse")); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java new file mode 100644 index 00000000000..62784a84a34 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodEarlyBeanProxyTests.java @@ -0,0 +1,192 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Tests that @Bean methods referenced from within @Feature methods + * get proxied early to avoid premature instantiation of actual + * bean instances. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureMethodEarlyBeanProxyTests { + + @Test + public void earlyProxyCreationAndBeanRegistrationLifecycle() { + AnnotationConfigApplicationContext ctx = + new AnnotationConfigApplicationContext(FeatureConfig.class); + + // + // see additional assertions in FeatureConfig#feature() + // + + // sanity check that all the bean definitions we expecting are present + assertThat(ctx.getBeanFactory().containsBeanDefinition("lazyHelperBean"), is(true)); + assertThat(ctx.getBeanFactory().containsBeanDefinition("eagerHelperBean"), is(true)); + assertThat(ctx.getBeanFactory().containsBeanDefinition("lazyPassthroughBean"), is(true)); + assertThat(ctx.getBeanFactory().containsBeanDefinition("eagerPassthroughBean"), is(true)); + + + // the lazy helper bean had methods invoked during feature method execution. it should be registered + assertThat(ctx.getBeanFactory().containsSingleton("lazyHelperBean"), is(true)); + + // the eager helper bean had methods invoked but should be registered in any case is it is non-lazy + assertThat(ctx.getBeanFactory().containsSingleton("eagerHelperBean"), is(true)); + + // the lazy passthrough bean was referenced in the feature method, but never invoked. it should not be registered + assertThat(ctx.getBeanFactory().containsSingleton("lazyPassthroughBean"), is(false)); + + // the eager passthrough bean should be registered in any case as it is non-lazy + assertThat(ctx.getBeanFactory().containsSingleton("eagerPassthroughBean"), is(true)); + + + // now actually fetch all the beans. none should be proxies + assertThat(ctx.getBean("lazyHelperBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(ctx.getBean("eagerHelperBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(ctx.getBean("lazyPassthroughBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + assertThat(ctx.getBean("eagerPassthroughBean"), not(instanceOf(EarlyBeanReferenceProxy.class))); + } + + + @Test + public void earlyProxyBeansMayBeInterfaceBasedOrConcrete() { + new AnnotationConfigApplicationContext(FeatureConfigReferencingNonInterfaceBeans.class); + } +} + + +@FeatureConfiguration +@Import(TestBeanConfig.class) +class FeatureConfig implements BeanFactoryAware { + private DefaultListableBeanFactory beanFactory; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (DefaultListableBeanFactory)beanFactory; + } + + @Feature + public StubSpecification feature(TestBeanConfig beans) { + + assertThat( + "The @Configuration class instance itself should be an early-ref proxy", + beans, instanceOf(EarlyBeanReferenceProxy.class)); + + // invocation of @Bean methods within @Feature methods should return proxies + ITestBean lazyHelperBean = beans.lazyHelperBean(); + ITestBean eagerHelperBean = beans.eagerHelperBean(); + ITestBean lazyPassthroughBean = beans.lazyPassthroughBean(); + ITestBean eagerPassthroughBean = beans.eagerPassthroughBean(); + + assertThat(lazyHelperBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(eagerHelperBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(lazyPassthroughBean, instanceOf(EarlyBeanReferenceProxy.class)); + assertThat(eagerPassthroughBean, instanceOf(EarlyBeanReferenceProxy.class)); + + // but at this point, the proxy instances should not have + // been registered as singletons with the container. + assertThat(this.beanFactory.containsSingleton("lazyHelperBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("eagerHelperBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("lazyPassthroughBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("eagerPassthroughBean"), is(false)); + + // invoking a method on the proxy should cause it to pass through + // to the container, instantiate the actual bean in question and + // register that actual underlying instance as a singleton. + assertThat(lazyHelperBean.getName(), equalTo("lazyHelper")); + assertThat(eagerHelperBean.getName(), equalTo("eagerHelper")); + + assertThat(this.beanFactory.containsSingleton("lazyHelperBean"), is(true)); + assertThat(this.beanFactory.containsSingleton("eagerHelperBean"), is(true)); + + // since no methods were called on the passthrough beans, they should remain + // uncreated / unregistered. + assertThat(this.beanFactory.containsSingleton("lazyPassthroughBean"), is(false)); + assertThat(this.beanFactory.containsSingleton("eagerPassthroughBean"), is(false)); + + return new StubSpecification(); + } +} + + +@Configuration +class TestBeanConfig { + + @Lazy @Bean + public ITestBean lazyHelperBean() { + return new TestBean("lazyHelper"); + } + + @Bean + public ITestBean eagerHelperBean() { + return new TestBean("eagerHelper"); + } + + @Lazy @Bean + public ITestBean lazyPassthroughBean() { + return new TestBean("lazyPassthrough"); + } + + @Bean + public ITestBean eagerPassthroughBean() { + return new TestBean("eagerPassthrough"); + } + +} + + +@FeatureConfiguration +@Import(NonInterfaceBeans.class) +class FeatureConfigReferencingNonInterfaceBeans { + @Feature + public FeatureSpecification feature1(NonInterfaceBeans beans) throws Throwable { + beans.testBean(); + return new StubSpecification(); + } + + @Feature + public FeatureSpecification feature2(TestBean testBean) throws Throwable { + return new StubSpecification(); + } +} + + +@Configuration +class NonInterfaceBeans { + @Bean + public TestBean testBean() { + return new TestBean("invalid"); + } +} + diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java new file mode 100644 index 00000000000..5ea134eb330 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/FeatureMethodQualifiedBeanReferenceTests.java @@ -0,0 +1,70 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.FeatureSpecification; + +import test.beans.ITestBean; +import test.beans.TestBean; + +/** + * Tests proving that @Feature methods may reference beans using @Qualifier + * as a parameter annotation. + * + * @author Chris Beams + * @since 3.1 + */ +public class FeatureMethodQualifiedBeanReferenceTests { + + @Test + public void test() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(Features.class, TestBeans.class); + ctx.refresh(); + } + + @FeatureConfiguration + static class Features { + + @Feature + public FeatureSpecification f(@Qualifier("testBean1") ITestBean testBean) { + assertThat(testBean.getName(), equalTo("one")); + return new StubSpecification(); + } + + } + + @Configuration + static class TestBeans { + @Bean + public ITestBean testBean1() { + return new TestBean("one"); + } + + @Bean + public ITestBean testBean2() { + return new TestBean("two"); + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java new file mode 100644 index 00000000000..4a06cf3e518 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/SimpleFeatureMethodProcessingTests.java @@ -0,0 +1,64 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.springframework.context.annotation.configuration.StubSpecification; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.util.Assert; + +/** + * Simple tests to ensure that @Feature methods are invoked and that the + * resulting returned {@link FeatureSpecification} object is delegated to + * the correct {@link FeatureSpecificationExecutor}. + * + * @author Chris Beams + * @since 3.1 + */ +public class SimpleFeatureMethodProcessingTests { + + @Test + public void test() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(FeatureConfig.class); + assertThat(MySpecificationExecutor.executeMethodWasCalled, is(false)); + ctx.refresh(); + assertThat(MySpecificationExecutor.executeMethodWasCalled, is(true)); + } + + @FeatureConfiguration + static class FeatureConfig { + @Feature + public FeatureSpecification f() { + return new StubSpecification(MySpecificationExecutor.class); + } + } + + static class MySpecificationExecutor implements FeatureSpecificationExecutor { + static boolean executeMethodWasCalled = false; + public void execute(FeatureSpecification spec, ExecutorContext executorContext) { + Assert.state(executeMethodWasCalled == false); + executeMethodWasCalled = true; + } + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index e5ab36f3404..566aa340e35 100644 --- a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -16,11 +16,13 @@ package org.springframework.context.annotation.configuration; -import static org.junit.Assert.*; -import org.junit.Test; -import test.beans.ITestBean; -import test.beans.TestBean; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Test; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Required; @@ -43,7 +45,9 @@ import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.annotation.Scope; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.core.PriorityOrdered; + +import test.beans.ITestBean; +import test.beans.TestBean; /** * Miscellaneous system tests covering {@link Bean} naming, aliases, scoping and error diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java new file mode 100644 index 00000000000..c81b3d9ab86 --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassWithPlaceholderConfigurerBeanTests.java @@ -0,0 +1,119 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.configuration; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Value; +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 test.beans.ITestBean; +import test.beans.TestBean; + +/** + * A configuration class that registers a placeholder configurer @Bean method + * cannot also have @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 @Value field. + * + * Therefore, placeholder configurers should be put in separate configuration classes + * as has been done in the test below. Simply said, placeholder configurer @Bean methods + * and @Value fields in the same configuration class are mutually exclusive. + * + * @author Chris Beams + */ +public class ConfigurationClassWithPlaceholderConfigurerBeanTests { + + /** + * Intentionally ignored test proving that a property placeholder bean + * cannot be declared in the same configuration class that has a @Value + * field in need of placeholder replacement. It's an obvious chicken-and-egg issue. + * The solution is to do as {@link #valueFieldsAreProcessedWhenPlaceholderConfigurerIsSegregated()} + * does and segragate the two bean definitions across configuration classes. + */ + @Ignore @Test + 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); + assertThat(testBean.getName(), nullValue()); + } + + @Test + 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(), equalTo("foo")); + } +} + +@Configuration +class ConfigWithValueField { + + @Value("${test.name}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } +} + +@Configuration +class ConfigWithPlaceholderConfigurer { + + @Bean + public PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + +} + +@Configuration +class ConfigWithValueFieldAndPlaceholderConfigurer { + + @Value("${test.name}") + private String name; + + @Bean + public ITestBean testBean() { + return new TestBean(this.name); + } + + @Bean + public PropertySourcesPlaceholderConfigurer ppc() { + return new PropertySourcesPlaceholderConfigurer(); + } + +} \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml new file mode 100644 index 00000000000..0418dc8251a --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodAndAutowiredFieldTests.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java new file mode 100644 index 00000000000..68baf7f7bfc --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/FeatureMethodLifecycleIssueTestSuite.java @@ -0,0 +1,36 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.configuration; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.context.annotation.FeatureMethodEarlyBeanProxyTests; + +/** + * Test suite that groups all tests related to @Feature method lifecycle issues. + * + * @author Chris Beams + */ +@RunWith(Suite.class) +@SuiteClasses({ + FeatureMethodEarlyBeanProxyTests.class, + ConfigurationClassWithPlaceholderConfigurerBeanTests.class, +}) +public class FeatureMethodLifecycleIssueTestSuite { + +} diff --git a/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java new file mode 100644 index 00000000000..94d7aef2bfd --- /dev/null +++ b/org.springframework.context/src/test/java/org/springframework/context/annotation/configuration/StubSpecification.java @@ -0,0 +1,46 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation.configuration; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.ExecutorContext; +import org.springframework.context.config.FeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; + +public class StubSpecification extends AbstractFeatureSpecification { + + public StubSpecification() { + this(StubSpecificationExecutor.class); + } + + public StubSpecification(Class excecutorType) { + super(excecutorType); + } + + @Override + protected void doValidate(SimpleProblemCollector reporter) { + } + +} + +class StubSpecificationExecutor implements FeatureSpecificationExecutor { + + public void execute(FeatureSpecification spec, ExecutorContext executorContext) { + } + +} diff --git a/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java b/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java index 1539f69d04e..9c4f6837aae 100644 --- a/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/format/datetime/joda/JodaTimeFormattingTests.java @@ -36,7 +36,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.MutablePropertyValues; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.convert.support.ConversionServiceFactory; +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; @@ -54,7 +54,7 @@ public class JodaTimeFormattingTests { @Before public void setUp() { - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); JodaTimeFormatterRegistrar registrar = new JodaTimeFormatterRegistrar(); registrar.registerFormatters(conversionService); diff --git a/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java b/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java index 510dbbfea48..7ee161efe62 100644 --- a/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java +++ b/org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -27,7 +27,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.MutablePropertyValues; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.convert.support.ConversionServiceFactory; +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; @@ -46,7 +46,7 @@ public class NumberFormattingTests { @Before public void setUp() { - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.setEmbeddedValueResolver(new StringValueResolver() { public String resolveStringValue(String strVal) { if ("${pattern}".equals(strVal)) { diff --git a/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java b/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java index ec27aa11fc6..9fe778dc926 100644 --- a/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java +++ b/org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -16,6 +16,9 @@ package org.springframework.format.support; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + import java.text.ParseException; import java.util.ArrayList; import java.util.Date; @@ -27,10 +30,8 @@ import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.joda.time.format.DateTimeFormat; import org.junit.After; -import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; - import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; @@ -40,7 +41,7 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.datetime.joda.DateTimeParser; import org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory; import org.springframework.format.datetime.joda.ReadablePartialPrinter; @@ -57,7 +58,7 @@ public class FormattingConversionServiceTests { @Before public void setUp() { formattingService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(formattingService); + DefaultConversionService.addDefaultConverters(formattingService); LocaleContextHolder.setLocale(Locale.US); } diff --git a/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java b/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java index 526f5a2ba3a..753748ff7d9 100644 --- a/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/org.springframework.context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -30,8 +30,6 @@ import java.util.TreeSet; import junit.framework.TestCase; -import org.junit.Ignore; -import org.junit.Test; import org.springframework.beans.BeanWithObjectProperty; import org.springframework.beans.DerivedTestBean; import org.springframework.beans.ITestBean; @@ -46,7 +44,7 @@ import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.context.support.StaticMessageSource; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.number.NumberFormatter; import org.springframework.format.support.FormattingConversionService; import org.springframework.util.StringUtils; @@ -319,7 +317,7 @@ public class DataBinderTests extends TestCase { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -350,7 +348,7 @@ public class DataBinderTests extends TestCase { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -372,7 +370,7 @@ public class DataBinderTests extends TestCase { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -393,7 +391,7 @@ public class DataBinderTests extends TestCase { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -415,7 +413,7 @@ public class DataBinderTests extends TestCase { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); binder.initDirectFieldAccess(); @@ -448,7 +446,7 @@ public class DataBinderTests extends TestCase { DataBinder binder = new DataBinder(tb); binder.initDirectFieldAccess(); FormattingConversionService conversionService = new FormattingConversionService(); - ConversionServiceFactory.addDefaultConverters(conversionService); + DefaultConversionService.addDefaultConverters(conversionService); conversionService.addFormatterForFieldType(Float.class, new NumberFormatter()); binder.setConversionService(conversionService); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -553,7 +551,7 @@ public class DataBinderTests extends TestCase { assertEquals(1, disallowedFields.length); assertEquals("age", disallowedFields[0]); - Map m = binder.getBindingResult().getModel(); + Map m = binder.getBindingResult().getModel(); assertTrue("There is one element in map", m.size() == 2); TestBean tb = (TestBean) m.get("person"); assertTrue("Same object", tb.equals(rod)); @@ -1415,6 +1413,7 @@ public class DataBinderTests extends TestCase { assertEquals("badName", nameError.getCode()); } + @SuppressWarnings("unchecked") public void testBindingWithResortedList() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java index 0b3664cf91f..c397ab55ba9 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java @@ -29,66 +29,16 @@ import org.springframework.core.convert.converter.GenericConverter; * * @author Keith Donald * @author Juergen Hoeller + * @author Chris Beams * @since 3.0 */ public abstract class ConversionServiceFactory { /** - * Create a new default ConversionService instance that can be safely modified. - */ - public static GenericConversionService createDefaultConversionService() { - GenericConversionService conversionService = new GenericConversionService(); - addDefaultConverters(conversionService); - return conversionService; - } - - /** - * Populate the given ConversionService instance with all applicable default converters. - */ - public static void addDefaultConverters(GenericConversionService conversionService) { - conversionService.addConverter(new ArrayToCollectionConverter(conversionService)); - conversionService.addConverter(new CollectionToArrayConverter(conversionService)); - - conversionService.addConverter(new ArrayToStringConverter(conversionService)); - conversionService.addConverter(new StringToArrayConverter(conversionService)); - - conversionService.addConverter(new ArrayToObjectConverter(conversionService)); - conversionService.addConverter(new ObjectToArrayConverter(conversionService)); - - conversionService.addConverter(new CollectionToStringConverter(conversionService)); - conversionService.addConverter(new StringToCollectionConverter(conversionService)); - - conversionService.addConverter(new CollectionToObjectConverter(conversionService)); - conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); - - conversionService.addConverter(new ArrayToArrayConverter(conversionService)); - conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); - conversionService.addConverter(new MapToMapConverter(conversionService)); - - conversionService.addConverter(new PropertiesToStringConverter()); - conversionService.addConverter(new StringToPropertiesConverter()); - - conversionService.addConverter(new StringToBooleanConverter()); - conversionService.addConverter(new StringToCharacterConverter()); - conversionService.addConverter(new StringToLocaleConverter()); - conversionService.addConverterFactory(new StringToNumberConverterFactory()); - conversionService.addConverterFactory(new StringToEnumConverterFactory()); - - conversionService.addConverter(new NumberToCharacterConverter()); - conversionService.addConverterFactory(new CharacterToNumberFactory()); - - conversionService.addConverterFactory(new NumberToNumberConverterFactory()); - - conversionService.addConverter(new ObjectToStringConverter()); - conversionService.addConverter(new ObjectToObjectConverter()); - conversionService.addConverter(new IdToEntityConverter(conversionService)); - } - - /** - * Register the given converter objects with the given target registry. + * 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 to register with + * @param registry the target registry */ public static void registerConverters(Set converters, ConverterRegistry registry) { if (converters != null) { @@ -110,4 +60,22 @@ public abstract class ConversionServiceFactory { } } + /** + * Create a new default ConversionService instance that can be safely modified. + * + * @deprecated in Spring 3.1 in favor of {@link DefaultConversionService#DefaultConversionService()} + */ + public static GenericConversionService createDefaultConversionService() { + return new DefaultConversionService(); + } + + /** + * Populate the given ConversionService instance with all applicable default converters. + * + * @deprecated in Spring 3.1 in favor of {@link DefaultConversionService#addDefaultConverters} + */ + public static void addDefaultConverters(GenericConversionService conversionService) { + DefaultConversionService.addDefaultConverters(conversionService); + } + } diff --git a/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java b/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java new file mode 100644 index 00000000000..1eea0c986aa --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java @@ -0,0 +1,86 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.convert.support; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; + +/** + * A specialization of {@link GenericConversionService} configured by default with + * converters appropriate for most applications. + * + *

Designed for direct instantiation but also exposes the static + * {@link #addDefaultConverters} utility method for ad hoc use against any + * {@code GenericConversionService} instance. + * + * @author Chris Beams + * @since 3.1 + */ +public class DefaultConversionService extends GenericConversionService { + + /** + * Create a new {@code DefaultConversionService} with the set of + * {@linkplain DefaultConversionService#addDefaultConverters default converters}. + */ + public DefaultConversionService() { + addDefaultConverters(this); + } + + /** + * Add converters appropriate for most environments. + * @param conversionService the service to register default formatters against + */ + public static void addDefaultConverters(GenericConversionService conversionService) { + conversionService.addConverter(new ArrayToCollectionConverter(conversionService)); + conversionService.addConverter(new CollectionToArrayConverter(conversionService)); + + conversionService.addConverter(new ArrayToStringConverter(conversionService)); + conversionService.addConverter(new StringToArrayConverter(conversionService)); + + conversionService.addConverter(new ArrayToObjectConverter(conversionService)); + conversionService.addConverter(new ObjectToArrayConverter(conversionService)); + + conversionService.addConverter(new CollectionToStringConverter(conversionService)); + conversionService.addConverter(new StringToCollectionConverter(conversionService)); + + conversionService.addConverter(new CollectionToObjectConverter(conversionService)); + conversionService.addConverter(new ObjectToCollectionConverter(conversionService)); + + conversionService.addConverter(new ArrayToArrayConverter(conversionService)); + conversionService.addConverter(new CollectionToCollectionConverter(conversionService)); + conversionService.addConverter(new MapToMapConverter(conversionService)); + + conversionService.addConverter(new PropertiesToStringConverter()); + conversionService.addConverter(new StringToPropertiesConverter()); + + conversionService.addConverter(new StringToBooleanConverter()); + conversionService.addConverter(new StringToCharacterConverter()); + conversionService.addConverter(new StringToLocaleConverter()); + conversionService.addConverterFactory(new StringToNumberConverterFactory()); + conversionService.addConverterFactory(new StringToEnumConverterFactory()); + + conversionService.addConverter(new NumberToCharacterConverter()); + conversionService.addConverterFactory(new CharacterToNumberFactory()); + + conversionService.addConverterFactory(new NumberToNumberConverterFactory()); + + conversionService.addConverter(new ObjectToStringConverter()); + conversionService.addConverter(new ObjectToObjectConverter()); + conversionService.addConverter(new IdToEntityConverter(conversionService)); + } + +} diff --git a/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java index b489bf6520c..35e20d9dbcd 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java +++ b/org.springframework.core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -24,11 +24,10 @@ import static org.springframework.util.SystemPropertyUtils.VALUE_SEPARATOR; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; - /** * Abstract base class for resolving properties against any underlying source. * @@ -39,7 +38,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe protected final Log logger = LogFactory.getLog(getClass()); - protected ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + protected ConversionService conversionService = new DefaultConversionService(); private PropertyPlaceholderHelper nonStrictHelper; private PropertyPlaceholderHelper strictHelper; diff --git a/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java b/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java index 32cacfb2ed3..433aad68992 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java +++ b/org.springframework.core/src/main/java/org/springframework/core/env/Environment.java @@ -69,7 +69,7 @@ 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, often based on * deployment environment. Profiles can be activated by setting {@linkplain - * AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME "spring.profiles.active"} as a system property + * AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME "spring.profile.active"} as a system property * or by calling {@link ConfigurableEnvironment#setActiveProfiles(String...)}. * *

If no profiles have explicitly been specified as active, then any 'default' profiles will implicitly diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java b/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java index b9417e8873d..caea4339e4b 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/MethodMetadata.java @@ -36,6 +36,11 @@ public interface MethodMetadata { */ String getMethodName(); + /** + * Return the fully-qualified name of the return type of the method. + */ + String getMethodReturnType(); + /** * Return the fully-qualified name of the class that declares this method. */ diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java b/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java index c6c8f28f80e..328e1f4318c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java @@ -58,7 +58,11 @@ public class StandardMethodMetadata implements MethodMetadata { public String getMethodName() { return this.introspectedMethod.getName(); } - + + public String getMethodReturnType() { + return this.introspectedMethod.getReturnType().getName(); + } + public String getDeclaringClassName() { return this.introspectedMethod.getDeclaringClass().getName(); } diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java index 311955368d1..77bec9fa47c 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/AnnotationMetadataReadingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -62,7 +62,7 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - return new MethodMetadataReadingVisitor(name, access, this.getClassName(), this.classLoader, this.methodMetadataMap); + return new MethodMetadataReadingVisitor(name, getReturnTypeFromAsmMethodDescriptor(desc), access, this.getClassName(), this.classLoader, this.methodMetadataMap); } @Override @@ -125,6 +125,19 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor } 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; + } + } result.put(entry.getKey(), value); } catch (Exception ex) { @@ -148,4 +161,46 @@ final class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor return annotatedMethods; } + /** + * Convert a type descriptor to a classname suitable for classloading with + * Class.forName(). + * + * @param typeDescriptor see ASM guide section 2.1.3 + */ + private static String convertAsmTypeDescriptorToClassName(String typeDescriptor) { + final String internalName; // See ASM guide section 2.1.2 + + if ("V".equals(typeDescriptor)) + return Void.class.getName(); + if ("I".equals(typeDescriptor)) + return Integer.class.getName(); + if ("Z".equals(typeDescriptor)) + return Boolean.class.getName(); + + // strip the leading array/object/primitive identifier + if (typeDescriptor.startsWith("[[")) + internalName = typeDescriptor.substring(3); + else if (typeDescriptor.startsWith("[")) + internalName = typeDescriptor.substring(2); + else + internalName = typeDescriptor.substring(1); + + // convert slashes to dots + String className = internalName.replace('/', '.'); + + // and strip trailing semicolon (if present) + if (className.endsWith(";")) + className = className.substring(0, internalName.length() - 1); + + return className; + } + + /** + * @param methodDescriptor see ASM guide section 2.1.4 + */ + private static String getReturnTypeFromAsmMethodDescriptor(String methodDescriptor) { + String returnTypeDescriptor = methodDescriptor.substring(methodDescriptor.indexOf(')') + 1); + return convertAsmTypeDescriptorToClassName(returnTypeDescriptor); + } + } diff --git a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java index 1bb40cbbee8..83f4a3dde01 100644 --- a/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java +++ b/org.springframework.core/src/main/java/org/springframework/core/type/classreading/MethodMetadataReadingVisitor.java @@ -43,6 +43,8 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method private final int access; + private String returnType; + private String declaringClassName; private final ClassLoader classLoader; @@ -51,10 +53,11 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method private final Map> attributeMap = new LinkedHashMap>(2); - public MethodMetadataReadingVisitor(String name, int access, String declaringClassName, ClassLoader classLoader, + public MethodMetadataReadingVisitor(String name, String returnType, int access, String declaringClassName, ClassLoader classLoader, MultiValueMap methodMetadataMap) { super(new EmptyVisitor()); this.name = name; + this.returnType = returnType; this.access = access; this.declaringClassName = declaringClassName; this.classLoader = classLoader; @@ -72,6 +75,10 @@ final class MethodMetadataReadingVisitor extends MethodAdapter implements Method return this.name; } + public String getMethodReturnType() { + return this.returnType; + } + public boolean isStatic() { return ((this.access & Opcodes.ACC_STATIC) != 0); } diff --git a/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java b/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java index c590dfa806d..16a59cbbad7 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/util/ObjectUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -32,6 +32,7 @@ import java.util.Arrays; * @author Keith Donald * @author Rod Johnson * @author Rob Harrop + * @author Chris Beams * @since 19.03.2004 * @see org.apache.commons.lang.ObjectUtils */ @@ -121,6 +122,54 @@ public abstract class ObjectUtils { 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 the product of a call to 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 the product of a call to 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 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( + String.format("constant [%s] does not exist in enum type %s", + constant, 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. diff --git a/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java b/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java index f7ab95038a5..c9588608faa 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/org.springframework.core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -372,6 +372,20 @@ public abstract class ReflectionUtils { return (method != null && method.getName().equals("toString") && method.getParameterTypes().length == 0); } + /** + * Determine whether the given method is originally declared by {@link java.lang.Object}. + */ + public static boolean isObjectMethod(Method method) { + try { + Object.class.getDeclaredMethod(method.getName(), method.getParameterTypes()); + return true; + } catch (SecurityException ex) { + return false; + } catch (NoSuchMethodException ex) { + return false; + } + } + /** * Make the given field accessible, explicitly setting it accessible if * necessary. The setAccessible(true) method is only called diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java index 2ac63df918d..7b7ff79d149 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/support/DefaultConversionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -55,7 +55,7 @@ import org.springframework.core.convert.converter.ConverterRegistry; */ public class DefaultConversionTests { - private ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + private ConversionService conversionService = new DefaultConversionService(); @Test public void testStringToCharacter() { diff --git a/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java b/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index cbe398d945b..1ab672dd93e 100644 --- a/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/org.springframework.core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -78,6 +78,7 @@ public class GenericConversionServiceTests { } @Test + @SuppressWarnings("rawtypes") public void addConverterNoSourceTargetClassInfoAvailable() { try { conversionService.addConverter(new Converter() { @@ -109,7 +110,7 @@ public class GenericConversionServiceTests { } public void convertNullTargetClass() { - assertNull(conversionService.convert("3", (Class) null)); + assertNull(conversionService.convert("3", (Class) null)); assertNull(conversionService.convert("3", TypeDescriptor.NULL)); } @@ -226,7 +227,7 @@ public class GenericConversionServiceTests { @Test public void testStringArrayToResourceArray() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MyStringArrayToResourceArrayConverter()); Resource[] converted = conversionService.convert(new String[] {"x1", "z3"}, Resource[].class); assertEquals(2, converted.length); @@ -236,7 +237,7 @@ public class GenericConversionServiceTests { @Test public void testStringArrayToIntegerArray() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MyStringArrayToIntegerArrayConverter()); Integer[] converted = conversionService.convert(new String[] {"x1", "z3"}, Integer[].class); assertEquals(2, converted.length); @@ -246,7 +247,7 @@ public class GenericConversionServiceTests { @Test public void testStringToIntegerArray() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MyStringToIntegerArrayConverter()); Integer[] converted = conversionService.convert("x1,z3", Integer[].class); assertEquals(2, converted.length); @@ -256,7 +257,7 @@ public class GenericConversionServiceTests { @Test public void testWildcardMap() throws Exception { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); Map input = new LinkedHashMap(); input.put("key", "value"); Object converted = conversionService.convert(input, new TypeDescriptor(getClass().getField("wildcardMap"))); @@ -265,14 +266,14 @@ public class GenericConversionServiceTests { @Test public void testListOfList() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); List> list = Collections.singletonList(Collections.singletonList("Foo")); assertNotNull(service.convert(list, String.class)); } @Test public void testStringToString() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); String value = "myValue"; String result = service.convert(value, String.class); assertSame(value, result); @@ -280,7 +281,7 @@ public class GenericConversionServiceTests { @Test public void testStringToObject() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); String value = "myValue"; Object result = service.convert(value, Object.class); assertSame(value, result); @@ -288,7 +289,7 @@ public class GenericConversionServiceTests { @Test public void testIgnoreCopyConstructor() { - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); WithCopyConstructor value = new WithCopyConstructor(); Object result = service.convert(value, WithCopyConstructor.class); assertSame(value, result); @@ -296,7 +297,7 @@ public class GenericConversionServiceTests { @Test public void testPerformance1() { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); StopWatch watch = new StopWatch("integer->string conversionPerformance"); watch.start("convert 4,000,000 with conversion service"); for (int i = 0; i < 4000000; i++) { @@ -313,7 +314,7 @@ public class GenericConversionServiceTests { @Test public void testPerformance2() throws Exception { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); StopWatch watch = new StopWatch("list -> list conversionPerformance"); watch.start("convert 4,000,000 with conversion service"); List source = new LinkedList(); @@ -340,7 +341,7 @@ public class GenericConversionServiceTests { @Test public void testPerformance3() throws Exception { - GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService conversionService = new DefaultConversionService(); StopWatch watch = new StopWatch("map -> map conversionPerformance"); watch.start("convert 4,000,000 with conversion service"); Map source = new HashMap(); @@ -387,6 +388,7 @@ public class GenericConversionServiceTests { TypeDescriptor sourceType = TypeDescriptor.forObject(list); TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyListDifferentTarget")); assertTrue(conversionService.canConvert(sourceType, targetType)); + @SuppressWarnings("unchecked") LinkedList result = (LinkedList) conversionService.convert(list, sourceType, targetType); assertEquals(LinkedList.class, result.getClass()); assertTrue(result.isEmpty()); @@ -437,6 +439,7 @@ public class GenericConversionServiceTests { TypeDescriptor sourceType = TypeDescriptor.forObject(map); TypeDescriptor targetType = new TypeDescriptor(getClass().getField("emptyMapDifferentTarget")); assertTrue(conversionService.canConvert(sourceType, targetType)); + @SuppressWarnings("unchecked") LinkedHashMap result = (LinkedHashMap) conversionService.convert(map, sourceType, targetType); assertEquals(map, result); assertEquals(LinkedHashMap.class, result.getClass()); diff --git a/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java index d929faa6922..fcfe1b38d40 100644 --- a/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/org.springframework.core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -16,6 +16,9 @@ package org.springframework.util; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + import java.io.IOException; import java.sql.SQLException; @@ -43,10 +46,10 @@ public final class ObjectUtilsTests extends TestCase { } public void testIsCompatibleWithThrowsClause() { - 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}; + 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}; assertTrue(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), null)); assertTrue(ObjectUtils.isCompatibleWithThrowsClause(new RuntimeException(), empty)); @@ -617,6 +620,35 @@ public final class ObjectUtilsTests extends TestCase { assertEquals("null", ObjectUtils.nullSafeToString((String[]) null)); } + enum Tropes { FOO, BAR, baz }; + + public void testContainsConstant() { + assertThat(ObjectUtils.containsConstant(Tropes.values(), "FOO"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "foo"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BaR"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "bar"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BAZ"), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "baz"), is(true)); + + assertThat(ObjectUtils.containsConstant(Tropes.values(), "BOGUS"), is(false)); + + assertThat(ObjectUtils.containsConstant(Tropes.values(), "FOO", true), is(true)); + assertThat(ObjectUtils.containsConstant(Tropes.values(), "foo", true), is(false)); + } + + public void testCaseInsensitiveValueOf() { + assertThat(ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "foo"), is(Tropes.FOO)); + assertThat(ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "BAR"), is(Tropes.BAR)); + try { + ObjectUtils.caseInsensitiveValueOf(Tropes.values(), "bogus"); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), + is("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); assertEquals(expected, actual); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java index e6223d86365..8a5d757f410 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -20,7 +20,7 @@ import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; @@ -45,7 +45,7 @@ public class StandardTypeConverter implements TypeConverter { public StandardTypeConverter() { synchronized (this) { if (defaultConversionService == null) { - defaultConversionService = ConversionServiceFactory.createDefaultConversionService(); + defaultConversionService = new DefaultConversionService(); } } this.conversionService = defaultConversionService; diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java index 6d148d4322d..c083f29109f 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ExpressionTestsUsingCoreConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -29,7 +29,7 @@ import org.junit.Test; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypeConverter; @@ -139,7 +139,7 @@ public class ExpressionTestsUsingCoreConversionService extends ExpressionTestCas */ private static class TypeConvertorUsingConversionService implements TypeConverter { - private final ConversionService service = ConversionServiceFactory.createDefaultConversionService(); + private final ConversionService service = new DefaultConversionService(); public Object convertValue(Object value, TypeDescriptor typeDescriptor) throws EvaluationException { return this.service.convert(value, typeDescriptor); diff --git a/org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java b/org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java new file mode 100644 index 00000000000..51bed0fac12 --- /dev/null +++ b/org.springframework.integration-tests/src/test/java/org/springframework/context/annotation/FeatureTestSuite.java @@ -0,0 +1,68 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + + +/** + * Tests directly or indirectly related to {@link FeatureConfiguration} class and + * {@link Feature} method processing. + * + * @author Chris Beams + * @since 3.1 + */ +/* + * commented due to classpath visibility differences between Eclipse + * and Ant/Ivy at the command line. Eclipse can see classes across + * project test folders, Ant/Ivy are not configured to do so. Uncomment + * as necessary when doing @Feature-related work. + * +@RunWith(Suite.class) +@SuiteClasses({ + EarlyBeanReferenceProxyCreatorTests.class, + SimpleFeatureMethodProcessingTests.class, + BeanFactoryAwareFeatureConfigurationTests.class, + FeatureMethodBeanReferenceTests.class, + FeatureMethodQualifiedBeanReferenceTests.class, + FeatureConfigurationClassTests.class, + FeatureMethodEarlyBeanProxyTests.class, + FeatureConfigurationImportTests.class, + FeatureConfigurationImportResourceTests.class, + + // context:component-scan related + ComponentScanFeatureTests.class, + ComponentScanSpecTests.class, + ComponentScanAnnotationTests.class, + ComponentScanAnnotationIntegrationTests.class, + + // tx-related + TxAnnotationDrivenFeatureTests.class, + TxNamespaceHandlerTests.class, + AnnotationTransactionNamespaceHandlerTests.class, + AnnotationDrivenTests.class, + + // mvc-related + AnnotationDrivenBeanDefinitionParserTests.class, + MvcAnnotationDrivenFeatureTests.class, + MvcViewControllersTests.class, + MvcResourcesTests.class, + MvcDefaultServletHandlerTests.class, + MvcNamespaceTests.class, +}) + */ +public class FeatureTestSuite { + +} \ No newline at end of file diff --git a/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java b/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java index ae91a4055f1..922b39b709d 100644 --- a/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java +++ b/org.springframework.integration-tests/src/test/java/org/springframework/expression/spel/support/BeanFactoryTypeConverter.java @@ -1,4 +1,21 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.expression.spel.support; + import java.beans.PropertyEditor; import org.springframework.beans.BeansException; @@ -8,7 +25,7 @@ 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.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.expression.TypeConverter; /** @@ -26,7 +43,7 @@ class BeanFactoryTypeConverter implements TypeConverter, BeanFactoryAware { public BeanFactoryTypeConverter() { synchronized (this) { if (defaultConversionService == null) { - defaultConversionService = ConversionServiceFactory.createDefaultConversionService(); + defaultConversionService = new DefaultConversionService(); } } this.conversionService = defaultConversionService; diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java index dd28db09596..cce36984602 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * 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. @@ -16,24 +16,18 @@ package org.springframework.transaction.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.transaction.annotation.AnnotationTransactionAttributeSource; -import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; -import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.context.config.ExecutorContext; +import org.w3c.dom.Element; /** - * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} - * implementation that allows users to easily configure all the infrastructure - * beans required to enable annotation-driven transaction demarcation. + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser + * BeanDefinitionParser} implementation that allows users to easily configure + * all the infrastructure beans required to enable annotation-driven transaction + * 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 @@ -43,7 +37,9 @@ import org.springframework.transaction.interceptor.TransactionInterceptor; * * @author Juergen Hoeller * @author Rob Harrop + * @author Chris Beams * @since 2.0 + * @see TxAnnotationDriven */ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { @@ -51,95 +47,43 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { * The bean name of the internally managed transaction advisor (mode="proxy"). */ public static final String TRANSACTION_ADVISOR_BEAN_NAME = - "org.springframework.transaction.config.internalTransactionAdvisor"; + TxAnnotationDrivenExecutor.TRANSACTION_ADVISOR_BEAN_NAME; /** * The bean name of the internally managed transaction aspect (mode="aspectj"). */ public static final String TRANSACTION_ASPECT_BEAN_NAME = - "org.springframework.transaction.config.internalTransactionAspect"; - - private static final String TRANSACTION_ASPECT_CLASS_NAME = - "org.springframework.transaction.aspectj.AnnotationTransactionAspect"; + TxAnnotationDrivenExecutor.TRANSACTION_ASPECT_BEAN_NAME; /** - * Parses the '<tx:annotation-driven/>' tag. Will + * Parses the {@code } tag. Will * {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary register an AutoProxyCreator} * with the container as necessary. */ public BeanDefinition parse(Element element, ParserContext parserContext) { - String mode = element.getAttribute("mode"); - if ("aspectj".equals(mode)) { - // mode="aspectj" - registerTransactionAspect(element, parserContext); - } - else { - // mode="proxy" - AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); - } + new TxAnnotationDriven(element.getAttribute("transaction-manager")) + .order(element.getAttribute("order")) + .mode(element.getAttribute("mode")) + .proxyTargetClass(Boolean.valueOf(element.getAttribute("proxy-target-class"))) + .source(parserContext.extractSource(element)) + .sourceName(element.getTagName()) + .execute(createExecutorContext(parserContext)); return null; } - private void registerTransactionAspect(Element element, ParserContext parserContext) { - if (!parserContext.getRegistry().containsBeanDefinition(TRANSACTION_ASPECT_BEAN_NAME)) { - RootBeanDefinition def = new RootBeanDefinition(); - def.setBeanClassName(TRANSACTION_ASPECT_CLASS_NAME); - def.setFactoryMethodName("aspectOf"); - registerTransactionManager(element, def); - parserContext.registerBeanComponent(new BeanComponentDefinition(def, TRANSACTION_ASPECT_BEAN_NAME)); - } - } - - private static void registerTransactionManager(Element element, BeanDefinition def) { - def.getPropertyValues().add("transactionManagerBeanName", - TxNamespaceHandler.getTransactionManagerName(element)); - } - - /** - * Inner class to just introduce an AOP framework dependency when actually in proxy mode. + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util */ - private static class AopAutoProxyConfigurer { - - public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { - AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); - - if (!parserContext.getRegistry().containsBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME)) { - Object eleSource = parserContext.extractSource(element); - - // Create the TransactionAttributeSource definition. - RootBeanDefinition sourceDef = new RootBeanDefinition(AnnotationTransactionAttributeSource.class); - sourceDef.setSource(eleSource); - sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); - - // Create the TransactionInterceptor definition. - RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); - interceptorDef.setSource(eleSource); - interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registerTransactionManager(element, interceptorDef); - interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); - String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); - - // Create the TransactionAttributeSourceAdvisor definition. - RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); - advisorDef.setSource(eleSource); - advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); - advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); - if (element.hasAttribute("order")) { - advisorDef.getPropertyValues().add("order", element.getAttribute("order")); - } - parserContext.getRegistry().registerBeanDefinition(TRANSACTION_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, TRANSACTION_ADVISOR_BEAN_NAME)); - parserContext.registerComponent(compositeDef); - } - } + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java index f90ca8b6b77..ba0a7eab788 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/JtaTransactionManagerBeanDefinitionParser.java @@ -74,7 +74,7 @@ public class JtaTransactionManagerBeanDefinitionParser extends AbstractSingleBea @Override protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { - return TxNamespaceHandler.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; + return TxAnnotationDriven.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; } } diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java index 9c2bea94a0c..a8a88ca5993 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAdviceBeanDefinitionParser.java @@ -37,50 +37,55 @@ import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; /** - * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} - * for the <tx:advice> tag. + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser + * BeanDefinitionParser} for the {@code } tag. * * @author Rob Harrop * @author Juergen Hoeller * @author Adrian Colyer + * @author Chris Beams * @since 2.0 */ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { - private static final String ATTRIBUTES = "attributes"; + private static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager"; - private static final String TIMEOUT = "timeout"; + private static final String METHOD_ELEMENT = "method"; - private static final String READ_ONLY = "read-only"; + private static final String METHOD_NAME_ATTRIBUTE = "name"; - private static final String NAME_MAP = "nameMap"; + private static final String ATTRIBUTES_ELEMENT = "attributes"; - private static final String PROPAGATION = "propagation"; + private static final String TIMEOUT_ATTRIBUTE = "timeout"; - private static final String ISOLATION = "isolation"; + private static final String READ_ONLY_ATTRIBUTE = "read-only"; - private static final String ROLLBACK_FOR = "rollback-for"; + private static final String PROPAGATION_ATTRIBUTE = "propagation"; - private static final String NO_ROLLBACK_FOR = "no-rollback-for"; + private static final String ISOLATION_ATTRIBUTE = "isolation"; + + private static final String ROLLBACK_FOR_ATTRIBUTE = "rollback-for"; + + private static final String NO_ROLLBACK_FOR_ATTRIBUTE = "no-rollback-for"; @Override - protected Class getBeanClass(Element element) { + protected Class getBeanClass(Element element) { return TransactionInterceptor.class; } @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { - builder.addPropertyReference("transactionManager", TxNamespaceHandler.getTransactionManagerName(element)); + builder.addPropertyReference("transactionManager", element.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE)); - List txAttributes = DomUtils.getChildElementsByTagName(element, ATTRIBUTES); + List txAttributes = DomUtils.getChildElementsByTagName(element, ATTRIBUTES_ELEMENT); if (txAttributes.size() > 1) { parserContext.getReaderContext().error( "Element is allowed at most once inside element ", element); } else if (txAttributes.size() == 1) { // Using attributes source. - Element attributeSourceElement = (Element) txAttributes.get(0); + Element attributeSourceElement = txAttributes.get(0); RootBeanDefinition attributeSourceDefinition = parseAttributeSource(attributeSourceElement, parserContext); builder.addPropertyValue("transactionAttributeSource", attributeSourceDefinition); } @@ -92,20 +97,21 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { } private RootBeanDefinition parseAttributeSource(Element attrEle, ParserContext parserContext) { - List methods = DomUtils.getChildElementsByTagName(attrEle, "method"); - ManagedMap transactionAttributeMap = new ManagedMap(methods.size()); + List methods = DomUtils.getChildElementsByTagName(attrEle, METHOD_ELEMENT); + ManagedMap transactionAttributeMap = + new ManagedMap(methods.size()); transactionAttributeMap.setSource(parserContext.extractSource(attrEle)); for (Element methodEle : methods) { - String name = methodEle.getAttribute("name"); + String name = methodEle.getAttribute(METHOD_NAME_ATTRIBUTE); TypedStringValue nameHolder = new TypedStringValue(name); nameHolder.setSource(parserContext.extractSource(methodEle)); RuleBasedTransactionAttribute attribute = new RuleBasedTransactionAttribute(); - String propagation = methodEle.getAttribute(PROPAGATION); - String isolation = methodEle.getAttribute(ISOLATION); - String timeout = methodEle.getAttribute(TIMEOUT); - String readOnly = methodEle.getAttribute(READ_ONLY); + String propagation = methodEle.getAttribute(PROPAGATION_ATTRIBUTE); + String isolation = methodEle.getAttribute(ISOLATION_ATTRIBUTE); + String timeout = methodEle.getAttribute(TIMEOUT_ATTRIBUTE); + String readOnly = methodEle.getAttribute(READ_ONLY_ATTRIBUTE); if (StringUtils.hasText(propagation)) { attribute.setPropagationBehaviorName(RuleBasedTransactionAttribute.PREFIX_PROPAGATION + propagation); } @@ -121,16 +127,16 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { } } if (StringUtils.hasText(readOnly)) { - attribute.setReadOnly(Boolean.valueOf(methodEle.getAttribute(READ_ONLY))); + attribute.setReadOnly(Boolean.valueOf(methodEle.getAttribute(READ_ONLY_ATTRIBUTE))); } List rollbackRules = new LinkedList(); - if (methodEle.hasAttribute(ROLLBACK_FOR)) { - String rollbackForValue = methodEle.getAttribute(ROLLBACK_FOR); + if (methodEle.hasAttribute(ROLLBACK_FOR_ATTRIBUTE)) { + String rollbackForValue = methodEle.getAttribute(ROLLBACK_FOR_ATTRIBUTE); addRollbackRuleAttributesTo(rollbackRules,rollbackForValue); } - if (methodEle.hasAttribute(NO_ROLLBACK_FOR)) { - String noRollbackForValue = methodEle.getAttribute(NO_ROLLBACK_FOR); + if (methodEle.hasAttribute(NO_ROLLBACK_FOR_ATTRIBUTE)) { + String noRollbackForValue = methodEle.getAttribute(NO_ROLLBACK_FOR_ATTRIBUTE); addNoRollbackRuleAttributesTo(rollbackRules,noRollbackForValue); } attribute.setRollbackRules(rollbackRules); @@ -140,7 +146,7 @@ class TxAdviceBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchTransactionAttributeSource.class); attributeSourceDefinition.setSource(parserContext.extractSource(attrEle)); - attributeSourceDefinition.getPropertyValues().add(NAME_MAP, transactionAttributeMap); + attributeSourceDefinition.getPropertyValues().add("nameMap", transactionAttributeMap); return attributeSourceDefinition; } diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java new file mode 100644 index 00000000000..299ed4854b4 --- /dev/null +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDriven.java @@ -0,0 +1,198 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.transaction.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.AdviceMode; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +public final class TxAnnotationDriven extends AbstractFeatureSpecification { + + static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; + + private static final Class EXECUTOR_TYPE = TxAnnotationDrivenExecutor.class; + + private Object txManager = null; + + private Object order = null; + + private Boolean proxyTargetClass = false; + + private Object mode = AdviceMode.PROXY; + + /** + * Create a {@code TxAnnotationDriven} specification assumes the presence of a + * {@link PlatformTransactionManager} bean named {@value #DEFAULT_TRANSACTION_MANAGER_BEAN_NAME}. + * + *

See the alternate constructors defined here if your transaction manager does + * not follow this default naming or you wish to refer to it by bean instance rather + * than by bean name. + * @see #TxAnnotationDriven(String) + * @see #TxAnnotationDriven(PlatformTransactionManager) + */ + public TxAnnotationDriven() { + this(DEFAULT_TRANSACTION_MANAGER_BEAN_NAME); + } + + /** + * Create a new {@code TxAnnotationDriven} specification that will use the specified + * transaction manager bean name. + * + * @param txManagerBeanName name of {@link PlatformTransactionManager} bean or a + * ${placeholder} or SpEL #{expression} resolving to bean name. If {@code null}, + * falls back to default value of {@value #DEFAULT_TRANSACTION_MANAGER_BEAN_NAME}. + */ + public TxAnnotationDriven(String txManagerBeanName) { + super(EXECUTOR_TYPE); + this.txManager = txManagerBeanName != null ? + txManagerBeanName : + DEFAULT_TRANSACTION_MANAGER_BEAN_NAME; + } + + /** + * Create a new TxAnnotationDriven specification that will use the specified transaction + * manager. + * + * @param txManager the {@link PlatformTransactionManager} bean to use. Must not be {@code null}. + */ + public TxAnnotationDriven(PlatformTransactionManager txManager) { + super(EXECUTOR_TYPE); + Assert.notNull(txManager, "transaction manager must not be null"); + this.txManager = txManager; + } + + /** + * Return the transaction manager to use. May be a {@link PlatformTransactionManager} + * instance or a String representing the bean name or a placeholder or SpEL expression + * that resolves to the bean name. + */ + Object transactionManager() { + return this.txManager; + } + + /** + * Indicate how transactional advice should be applied. + * @see AdviceMode + */ + public TxAnnotationDriven mode(AdviceMode mode) { + this.mode = mode; + return this; + } + + /** + * Indicate how transactional advice should be applied. + * @param name matching one of the labels in the AdviceMode enum; + * placeholder and SpEL expressions are not allowed. + * @see AdviceMode + */ + TxAnnotationDriven mode(String mode) { + if (StringUtils.hasText(mode)) { + this.mode = mode; + } + return this; + } + + /** + * Return how transactional advice should be applied. + */ + AdviceMode mode() { + if (this.mode instanceof AdviceMode) { + return (AdviceMode)this.mode; + } + if (this.mode instanceof String) { + return ObjectUtils.caseInsensitiveValueOf(AdviceMode.values(), (String)this.mode); + } + // TODO SPR-7420: deal with in validate & raise problem + throw new IllegalStateException( + "invalid type for field 'mode' (must be of type AdviceMode or String): " + + this.mode.getClass().getName()); + } + + /** + * Indicate whether class-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. + * + *

Note: Class-based proxies require the {@link Transactional @Transactional} + * annotation to be defined on the concrete class. Annotations in interfaces will + * not work in that case (they will rather only work with interface-based proxies)! + */ + public TxAnnotationDriven proxyTargetClass(Boolean proxyTargetClass) { + this.proxyTargetClass = proxyTargetClass; + return this; + } + + /** + * Return whether class-based (CGLIB) proxies are to be created as opposed + * to standard Java interface-based proxies. + */ + Boolean proxyTargetClass() { + return this.proxyTargetClass; + } + + /** + * Indicate the ordering of the execution of the transaction advisor + * when multiple advice executes at a specific joinpoint. The default is + * {@code null}, indicating that default ordering should be used. + */ + public TxAnnotationDriven order(int order) { + this.order = order; + return this; + } + + /** + * Indicate the ordering of the execution of the transaction advisor + * when multiple advice executes at a specific joinpoint. The default is + * {@code null}, indicating that default ordering should be used. + */ + public TxAnnotationDriven order(String order) { + if (StringUtils.hasText(order)) { + this.order = order; + } + return this; + } + + /** + * Return the ordering of the execution of the transaction advisor + * when multiple advice executes at a specific joinpoint. May return + * {@code null}, indicating that default ordering should be used. + */ + Object order() { + return this.order; + } + + @Override + protected void doValidate(SimpleProblemCollector problems) { + if (this.mode instanceof String) { + if (!ObjectUtils.containsConstant(AdviceMode.values(), (String)this.mode)) { + problems.error("no such mode name: " + this.mode); + } + } + } + +} \ No newline at end of file diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java new file mode 100644 index 00000000000..cf971c0c672 --- /dev/null +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxAnnotationDrivenExecutor.java @@ -0,0 +1,139 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.transaction.config; + +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.ComponentRegistrar; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.util.Assert; + +/** + * TODO SPR-7420: document + * + * @author Chris Beams + * @since 3.1 + */ +final class TxAnnotationDrivenExecutor extends AbstractSpecificationExecutor { + + /** + * The bean name of the internally managed transaction advisor (used when mode == PROXY). + */ + public static final String TRANSACTION_ADVISOR_BEAN_NAME = + "org.springframework.transaction.config.internalTransactionAdvisor"; + + /** + * The bean name of the internally managed transaction aspect (used when mode == ASPECTJ). + */ + public static final String TRANSACTION_ASPECT_BEAN_NAME = + "org.springframework.transaction.config.internalTransactionAspect"; + + private static final String TRANSACTION_ASPECT_CLASS_NAME = + "org.springframework.transaction.aspectj.AnnotationTransactionAspect"; + + + @Override + protected void doExecute(TxAnnotationDriven txSpec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + switch (txSpec.mode()) { + case ASPECTJ: + registerTransactionAspect(txSpec, registry, registrar); + break; + case PROXY: + AopAutoProxyConfigurer.configureAutoProxyCreator(txSpec, registry, registrar); + break; + default: + throw new IllegalArgumentException( + String.format("AdviceMode %s is not supported", txSpec.mode())); + } + } + + private void registerTransactionAspect(TxAnnotationDriven spec, BeanDefinitionRegistry registry, ComponentRegistrar registrar) { + if (!registry.containsBeanDefinition(TRANSACTION_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(TRANSACTION_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + registerTransactionManager(spec, def); + registrar.registerBeanComponent(new BeanComponentDefinition(def, TRANSACTION_ASPECT_BEAN_NAME)); + } + } + + private static void registerTransactionManager(TxAnnotationDriven spec, BeanDefinition def) { + Object txManager = spec.transactionManager(); + Assert.notNull(txManager, "transactionManager must be specified"); + if (txManager instanceof String) { + def.getPropertyValues().add("transactionManagerBeanName", txManager); + } else { + def.getPropertyValues().add("transactionManager", txManager); + } + } + + /** + * Inner class to just introduce an AOP framework dependency when actually in proxy mode. + */ + private static class AopAutoProxyConfigurer { + + public static void configureAutoProxyCreator(TxAnnotationDriven txSpec, BeanDefinitionRegistry registry, ComponentRegistrar registrar) { + Object source = txSpec.source(); + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(registry, registrar, source, txSpec.proxyTargetClass()); + + if (!registry.containsBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME)) { + + // Create the TransactionAttributeSource definition. + RootBeanDefinition sourceDef = new RootBeanDefinition(AnnotationTransactionAttributeSource.class); + sourceDef.setSource(source); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String sourceName = registrar.registerWithGeneratedName(sourceDef); + + // Create the TransactionInterceptor definition. + RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); + interceptorDef.setSource(source); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registerTransactionManager(txSpec, interceptorDef); + interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + String interceptorName = registrar.registerWithGeneratedName(interceptorDef); + + // Create the TransactionAttributeSourceAdvisor definition. + RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); + advisorDef.setSource(source); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (txSpec.order() != null) { + advisorDef.getPropertyValues().add("order", txSpec.order()); + } + registry.registerBeanDefinition(TRANSACTION_ADVISOR_BEAN_NAME, advisorDef); + + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(txSpec.sourceName(), source); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, TRANSACTION_ADVISOR_BEAN_NAME)); + registrar.registerComponent(compositeDef); + } + } + } +} diff --git a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java index ac657898304..bdf376f2473 100644 --- a/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java +++ b/org.springframework.transaction/src/main/java/org/springframework/transaction/config/TxNamespaceHandler.java @@ -39,17 +39,6 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; */ public class TxNamespaceHandler extends NamespaceHandlerSupport { - static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager"; - - static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager"; - - - static String getTransactionManagerName(Element element) { - return (element.hasAttribute(TRANSACTION_MANAGER_ATTRIBUTE) ? - element.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE) : DEFAULT_TRANSACTION_MANAGER_BEAN_NAME); - } - - public void init() { registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); diff --git a/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java index 86921c7c57e..2b813663196 100644 --- a/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java +++ b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionNamespaceHandlerTests.java @@ -19,6 +19,7 @@ package org.springframework.transaction.annotation; import java.lang.management.ManagementFactory; import java.util.Collection; import java.util.Map; + import javax.management.MBeanServer; import javax.management.ObjectName; diff --git a/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java new file mode 100644 index 00000000000..7071426d260 --- /dev/null +++ b/org.springframework.transaction/src/test/java/org/springframework/transaction/annotation/TxAnnotationDrivenFeatureTests.java @@ -0,0 +1,147 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.transaction.annotation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.context.config.AdviceMode; +import org.springframework.stereotype.Service; +import org.springframework.transaction.CallCountingTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.AnnotationTransactionNamespaceHandlerTests.TransactionalTestBean; +import org.springframework.transaction.config.TxAnnotationDriven; + +/** + * Integration tests for {@link TxAnnotationDriven} support within @Configuration + * classes. Adapted from original tx: namespace tests at + * {@link AnnotationTransactionNamespaceHandlerTests}. + * + * @author Chris Beams + * @since 3.1 + */ +public class TxAnnotationDrivenFeatureTests { + @Test + public void transactionProxyIsCreated() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TxFeature.class, TxManagerConfig.class); + ctx.refresh(); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + assertThat("testBean is not a proxy", AopUtils.isAopProxy(bean), is(true)); + Map services = ctx.getBeansWithAnnotation(Service.class); + assertThat("Stereotype annotation not visible", services.containsKey("testBean"), is(true)); + } + + @Test + public void txManagerIsResolvedOnInvocationOfTransactionalMethod() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TxFeature.class, TxManagerConfig.class); + ctx.refresh(); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + + // invoke a transactional method, causing the PlatformTransactionManager bean to be resolved. + bean.findAllFoos(); + } + + @Test + public void txManagerIsResolvedCorrectlyWhenMultipleManagersArePresent() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TxFeature.class, MultiTxManagerConfig.class); + ctx.refresh(); + TransactionalTestBean bean = ctx.getBean(TransactionalTestBean.class); + + // invoke a transactional method, causing the PlatformTransactionManager bean to be resolved. + bean.findAllFoos(); + } + + /** + * A cheap test just to prove that in ASPECTJ mode, the AnnotationTransactionAspect does indeed + * get loaded -- or in this case, attempted to be loaded at which point the test fails. + */ + @Test + public void proxyTypeAspectJCausesRegistrationOfAnnotationTransactionAspect() { + try { + new AnnotationConfigApplicationContext(TxWithAspectJFeature.class, TxManagerConfig.class); + fail("should have thrown CNFE when trying to load AnnotationTransactionAspect. " + + "Do you actually have org.springframework.aspects on the classpath?"); + } catch (CannotLoadBeanClassException ex) { + ClassNotFoundException cause = (ClassNotFoundException) ex.getCause(); + assertThat(cause.getMessage(), equalTo("org.springframework.transaction.aspectj.AnnotationTransactionAspect")); + } + } + +} + +@FeatureConfiguration +class TxFeature { + + @Feature + public TxAnnotationDriven tx(TxManagerConfig txManagerConfig) { + return new TxAnnotationDriven(txManagerConfig.txManager()); + } +} + + +@FeatureConfiguration +class TxWithAspectJFeature { + + @Feature + public TxAnnotationDriven tx(PlatformTransactionManager txManager) { + return new TxAnnotationDriven(txManager).mode(AdviceMode.ASPECTJ); + } + +} + + +@Configuration +class TxManagerConfig { + + @Bean + public TransactionalTestBean testBean() { + return new TransactionalTestBean(); + } + + @Bean + public PlatformTransactionManager txManager() { + return new CallCountingTransactionManager(); + } + +} + + +@Configuration +class MultiTxManagerConfig extends TxManagerConfig { + + @Bean + public PlatformTransactionManager txManager2() { + return new CallCountingTransactionManager(); + } + +} + diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java deleted file mode 100644 index 13f10cf673b..00000000000 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AbstractHttpRequestHandlerBeanDefinitionParser.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.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; -import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; - -/** - * Abstract base class for {@link BeanDefinitonParser}s that register an HttpRequestHandler. - * - * @author Jeremy Grelle - * @since 3.0.4 - */ -abstract class AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser{ - - private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; - - public BeanDefinition parse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - registerHandlerAdapterIfNecessary(parserContext, source); - doParse(element, parserContext); - return null; - } - - public abstract void doParse(Element element, ParserContext parserContext); - - private void registerHandlerAdapterIfNecessary(ParserContext parserContext, Object source) { - if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { - RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(HttpRequestHandlerAdapter.class); - handlerAdapterDef.setSource(source); - handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - parserContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); - } - } - -} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 9965b8c9e10..6a9ea3af459 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -18,264 +18,86 @@ package org.springframework.web.servlet.config; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -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.ManagedList; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.core.convert.ConversionService; -import org.springframework.format.support.FormattingConversionServiceFactoryBean; -import org.springframework.http.converter.ByteArrayHttpMessageConverter; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.ResourceHttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; -import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; -import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; -import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; -import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; -import org.springframework.http.converter.xml.SourceHttpMessageConverter; -import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; -import org.springframework.util.ClassUtils; +import org.springframework.context.config.ExecutorContext; import org.springframework.util.xml.DomUtils; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; -import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; -import org.springframework.web.servlet.handler.MappedInterceptor; -import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; -import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver; -import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; -import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; -import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.w3c.dom.Element; /** - * {@link BeanDefinitionParser} that parses the {@code annotation-driven} element to configure a Spring MVC web - * application. + * {@link BeanDefinitionParser} that parses the {@code annotation-driven} element + * to configure a Spring MVC web application. * - *

Responsible for: - *

    - *
  1. Registering a DefaultAnnotationHandlerMapping bean for mapping HTTP Servlet Requests to @Controller methods - * using @RequestMapping annotations. - *
  2. Registering a AnnotationMethodHandlerAdapter bean for invoking annotated @Controller methods. - * Will configure the HandlerAdapter's webBindingInitializer property for centrally configuring - * {@code @Controller} {@code DataBinder} instances: - *
      - *
    • Configures the conversionService if specified, otherwise defaults to a fresh {@link ConversionService} instance - * created by the default {@link FormattingConversionServiceFactoryBean}. - *
    • Configures the validator if specified, otherwise defaults to a fresh {@link Validator} instance created by the - * default {@link LocalValidatorFactoryBean} if the JSR-303 API is present on the classpath. - *
    • Configures standard {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}, - * including the {@link Jaxb2RootElementHttpMessageConverter} if JAXB2 is present on the classpath, and - * the {@link MappingJacksonHttpMessageConverter} if Jackson is present on the classpath. - *
    - *
- * - * @author Keith Donald - * @author Juergen Hoeller - * @author Arjen Poutsma * @author Rossen Stoyanchev * @since 3.0 + * @see MvcAnnotationDriven + * @see MvcAnnotationDrivenExecutor */ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { - private static final boolean jsr303Present = ClassUtils.isPresent( - "javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - private static final boolean jaxb2Present = - ClassUtils.isPresent("javax.xml.bind.Binder", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - private static final boolean jacksonPresent = - ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) && - ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - private static boolean romePresent = - ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); - - + /** + * Parses the {@code } tag. + */ public BeanDefinition parse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - - CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); - parserContext.pushContainingComponent(compDefinition); - - RootBeanDefinition annMappingDef = new RootBeanDefinition(DefaultAnnotationHandlerMapping.class); - annMappingDef.setSource(source); - annMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - annMappingDef.getPropertyValues().add("order", 0); - String annMappingName = parserContext.getReaderContext().registerWithGeneratedName(annMappingDef); - - RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); - RuntimeBeanReference validator = getValidator(element, source, parserContext); - RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element, source, parserContext); - - RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); - bindingDef.setSource(source); - bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - bindingDef.getPropertyValues().add("conversionService", conversionService); - bindingDef.getPropertyValues().add("validator", validator); - bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver); - - ManagedList messageConverters = getMessageConverters(element, source, parserContext); - ManagedList argumentResolvers = getArgumentResolvers(element, source, parserContext); - - RootBeanDefinition annAdapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); - annAdapterDef.setSource(source); - annAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - annAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); - annAdapterDef.getPropertyValues().add("messageConverters", messageConverters); - if (argumentResolvers != null) { - annAdapterDef.getPropertyValues().add("customArgumentResolvers", argumentResolvers); - } - String annAdapterName = parserContext.getReaderContext().registerWithGeneratedName(annAdapterDef); - - RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); - csInterceptorDef.setSource(source); - csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService); - RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class); - mappedCsInterceptorDef.setSource(source); - mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null); - mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef); - String mappedInterceptorName = parserContext.getReaderContext().registerWithGeneratedName(mappedCsInterceptorDef); - - RootBeanDefinition annExceptionResolver = new RootBeanDefinition(AnnotationMethodHandlerExceptionResolver.class); - annExceptionResolver.setSource(source); - annExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - annExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); - annExceptionResolver.getPropertyValues().add("order", 0); - String annExceptionResolverName = - parserContext.getReaderContext().registerWithGeneratedName(annExceptionResolver); - - RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class); - responseStatusExceptionResolver.setSource(source); - responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - responseStatusExceptionResolver.getPropertyValues().add("order", 1); - String responseStatusExceptionResolverName = - parserContext.getReaderContext().registerWithGeneratedName(responseStatusExceptionResolver); - - RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class); - defaultExceptionResolver.setSource(source); - defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - defaultExceptionResolver.getPropertyValues().add("order", 2); - String defaultExceptionResolverName = - parserContext.getReaderContext().registerWithGeneratedName(defaultExceptionResolver); - - parserContext.registerComponent(new BeanComponentDefinition(annMappingDef, annMappingName)); - parserContext.registerComponent(new BeanComponentDefinition(annAdapterDef, annAdapterName)); - parserContext.registerComponent(new BeanComponentDefinition(annExceptionResolver, annExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(responseStatusExceptionResolver, responseStatusExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); - parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); - parserContext.popAndRegisterContainingComponent(); - + MvcAnnotationDriven spec = createSpecification(element, parserContext); + spec.execute(createExecutorContext(parserContext)); return null; } - - private RuntimeBeanReference getConversionService(Element element, Object source, ParserContext parserContext) { - RuntimeBeanReference conversionServiceRef; + + private MvcAnnotationDriven createSpecification(Element element, ParserContext parserContext) { + MvcAnnotationDriven spec = new MvcAnnotationDriven(); if (element.hasAttribute("conversion-service")) { - conversionServiceRef = new RuntimeBeanReference(element.getAttribute("conversion-service")); + String conversionService = element.getAttribute("conversion-service"); + spec.conversionService(conversionService); } - else { - RootBeanDefinition conversionDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); - conversionDef.setSource(source); - conversionDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String conversionName = parserContext.getReaderContext().registerWithGeneratedName(conversionDef); - parserContext.registerComponent(new BeanComponentDefinition(conversionDef, conversionName)); - conversionServiceRef = new RuntimeBeanReference(conversionName); - } - return conversionServiceRef; - } - - private RuntimeBeanReference getValidator(Element element, Object source, ParserContext parserContext) { if (element.hasAttribute("validator")) { - return new RuntimeBeanReference(element.getAttribute("validator")); + spec.validator(element.getAttribute("validator")); } - else if (jsr303Present) { - RootBeanDefinition validatorDef = new RootBeanDefinition(LocalValidatorFactoryBean.class); - validatorDef.setSource(source); - validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef); - parserContext.registerComponent(new BeanComponentDefinition(validatorDef, validatorName)); - return new RuntimeBeanReference(validatorName); - } - else { - return null; - } - } - - private RuntimeBeanReference getMessageCodesResolver(Element element, Object source, ParserContext parserContext) { if (element.hasAttribute("message-codes-resolver")) { - return new RuntimeBeanReference(element.getAttribute("message-codes-resolver")); - } else { - return null; + spec.messageCodesResolver(element.getAttribute("message-codes-resolver")); } - } - - private ManagedList getArgumentResolvers(Element element, Object source, ParserContext parserContext) { - Element resolversElement = DomUtils.getChildElementByTagName(element, "argument-resolvers"); - if (resolversElement != null) { - ManagedList argumentResolvers = new ManagedList(); - argumentResolvers.setSource(source); - for (Element resolver : DomUtils.getChildElementsByTagName(resolversElement, "bean")) { - BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(resolver); - beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(resolver, beanDef); - argumentResolvers.add(beanDef); - } - return argumentResolvers; - } - return null; - } - - private ManagedList getMessageConverters(Element element, Object source, ParserContext parserContext) { Element convertersElement = DomUtils.getChildElementByTagName(element, "message-converters"); if (convertersElement != null) { - ManagedList messageConverters = new ManagedList(); - messageConverters.setSource(source); - for (Element converter : DomUtils.getChildElementsByTagName(convertersElement, "bean")) { - BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(converter); - beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(converter, beanDef); - messageConverters.add(beanDef); + if (convertersElement.hasAttribute("register-defaults")) { + spec.shouldRegisterDefaultMessageConverters(Boolean.valueOf(convertersElement + .getAttribute("register-defaults"))); } - return messageConverters; - } else { - ManagedList messageConverters = new ManagedList(); - messageConverters.setSource(source); - messageConverters.add(createConverterBeanDefinition(ByteArrayHttpMessageConverter.class, source)); - - RootBeanDefinition stringConverterDef = createConverterBeanDefinition(StringHttpMessageConverter.class, - source); - stringConverterDef.getPropertyValues().add("writeAcceptCharset", false); - messageConverters.add(stringConverterDef); - - messageConverters.add(createConverterBeanDefinition(ResourceHttpMessageConverter.class, source)); - messageConverters.add(createConverterBeanDefinition(SourceHttpMessageConverter.class, source)); - messageConverters.add(createConverterBeanDefinition(XmlAwareFormHttpMessageConverter.class, source)); - if (jaxb2Present) { - messageConverters - .add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); - } - if (jacksonPresent) { - messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source)); - } - if (romePresent) { - messageConverters.add(createConverterBeanDefinition(AtomFeedHttpMessageConverter.class, source)); - messageConverters.add(createConverterBeanDefinition(RssChannelHttpMessageConverter.class, source)); - } - return messageConverters; + spec.messageConverters(extractBeanSubElements(convertersElement, parserContext)); } + Element resolversElement = DomUtils.getChildElementByTagName(element, "argument-resolvers"); + if (resolversElement != null) { + spec.argumentResolvers(extractBeanSubElements(resolversElement, parserContext)); + } + + spec.source(parserContext.extractSource(element)); + spec.sourceName(element.getTagName()); + return spec; } - private RootBeanDefinition createConverterBeanDefinition(Class converterClass, - Object source) { - RootBeanDefinition beanDefinition = new RootBeanDefinition(converterClass); - beanDefinition.setSource(source); - beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - - return beanDefinition; + private ManagedList extractBeanSubElements(Element parentElement, ParserContext parserContext) { + ManagedList list = new ManagedList(); + list.setSource(parserContext.extractSource(parentElement)); + for (Element beanElement : DomUtils.getChildElementsByTagName(parentElement, "bean")) { + BeanDefinitionHolder beanDef = parserContext.getDelegate().parseBeanDefinitionElement(beanElement); + beanDef = parserContext.getDelegate().decorateBeanDefinitionIfRequired(beanElement, beanDef); + list.add(beanDef); + } + return list; } + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java index fe60722234d..a0b05042d32 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/DefaultServletHandlerBeanDefinitionParser.java @@ -16,20 +16,15 @@ package org.springframework.web.servlet.config; -import java.util.Map; - -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.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.config.ExecutorContext; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; +import org.w3c.dom.Element; /** * {@link BeanDefinitionParser} that parses a {@code default-servlet-handler} element to @@ -37,37 +32,39 @@ import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler * {@link SimpleUrlHandlerMapping} for mapping resource requests, and a * {@link HttpRequestHandlerAdapter} if necessary. * - * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 3.0.4 */ -class DefaultServletHandlerBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser { +class DefaultServletHandlerBeanDefinitionParser implements BeanDefinitionParser { - @Override - public void doParse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - - String defaultServletName = element.getAttribute("default-servlet-name"); - RootBeanDefinition defaultServletHandlerDef = new RootBeanDefinition(DefaultServletHttpRequestHandler.class); - defaultServletHandlerDef.setSource(source); - defaultServletHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - if (StringUtils.hasText(defaultServletName)) { - defaultServletHandlerDef.getPropertyValues().add("defaultServletName", defaultServletName); - } - String defaultServletHandlerName = parserContext.getReaderContext().generateBeanName(defaultServletHandlerDef); - parserContext.getRegistry().registerBeanDefinition(defaultServletHandlerName, defaultServletHandlerDef); - parserContext.registerComponent(new BeanComponentDefinition(defaultServletHandlerDef, defaultServletHandlerName)); - - Map urlMap = new ManagedMap(); - urlMap.put("/**", defaultServletHandlerName); - - RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); - handlerMappingDef.setSource(source); - handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - - String handlerMappingBeanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); - parserContext.getRegistry().registerBeanDefinition(handlerMappingBeanName, handlerMappingDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, handlerMappingBeanName)); + /** + * Parses the {@code } tag. + */ + public BeanDefinition parse(Element element, ParserContext parserContext) { + MvcDefaultServletHandler spec = createSpecification(element, parserContext); + spec.execute(createExecutorContext(parserContext)); + return null; + } + + private MvcDefaultServletHandler createSpecification(Element element, ParserContext parserContext) { + String defaultServletHandler = element.getAttribute("default-servlet-handler"); + MvcDefaultServletHandler spec = StringUtils.hasText(defaultServletHandler) ? new MvcDefaultServletHandler( + defaultServletHandler) : new MvcDefaultServletHandler(); + return spec; + } + + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java new file mode 100644 index 00000000000..d975321ea82 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDriven.java @@ -0,0 +1,249 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.support.WebArgumentResolver; + +/** + * Specifies the Spring MVC "annotation-driven" container feature. The + * feature provides the following fine-grained configuration: + * + *
    + *
  • {@code DefaultAnnotationHandlerMapping} bean for mapping HTTP Servlet Requests + * to {@code @Controller} methods using {@code @RequestMapping} annotations. + *
  • {@code AnnotationMethodHandlerAdapter} bean for invoking annotated + * {@code @Controller} methods. + *
  • {@code HandlerExceptionResolver} beans for invoking {@code @ExceptionHandler} + * controller methods and for mapping Spring exception to HTTP status codes. + *
+ * + *

The {@code HandlerAdapter} is further configured with the following, which apply + * globally (across controllers invoked though the {@code AnnotationMethodHandlerAdapter}): + * + *

    + *
  • {@link ConversionService} - a custom instance can be provided via + * {@link #conversionService(ConversionService)}. Otherwise it defaults to a fresh + * {@link ConversionService} instance created by the default + * {@link FormattingConversionServiceFactoryBean}. + *
  • {@link Validator} - a custom instance can be provided via + * {@link #validator(Validator)}. Otherwise it defaults to a fresh {@code Validator} + * instance created by the default {@link LocalValidatorFactoryBean} assuming + * JSR-303 API is present on the classpath. + *
  • {@code HttpMessageConverter} beans including the {@link + * Jaxb2RootElementHttpMessageConverter} assuming JAXB2 is present on the + * classpath, the {@link MappingJacksonHttpMessageConverter} assuming Jackson + * is present on the classpath, and the {@link AtomFeedHttpMessageConverter} and the + * {@link RssChannelHttpMessageConverter} converters assuming Rome is present on + * the classpath. + *
  • Optionally, custom {@code WebArgumentResolver} beans to use for resolving + * custom arguments to handler methods. These are typically implemented to detect + * special parameter types, resolving well-known argument values for them. + *
+ * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public final class MvcAnnotationDriven extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcAnnotationDrivenExecutor.class; + + private Object conversionService; + + private Object validator; + + private Object messageCodesResolver; + + private boolean shouldRegisterDefaultMessageConverters = true; + + private ManagedList messageConverters = new ManagedList(); + + private ManagedList argumentResolvers = new ManagedList(); + + /** + * Creates an MvcAnnotationDriven specification. + */ + public MvcAnnotationDriven() { + super(EXECUTOR_TYPE); + } + + /** + *

The ConversionService bean instance to use for type conversion during + * field binding. This is not required input. It only needs to be provided + * explicitly if custom converters or formatters need to be configured. + * + *

If not provided, a default FormattingConversionService is registered + * that contains converters to/from standard JDK types. In addition, full + * support for date/time formatting will be installed if the Joda Time + * library is present on the classpath. + * + * @param conversionService the ConversionService instance to use + */ + public MvcAnnotationDriven conversionService(ConversionService conversionService) { + this.conversionService = conversionService; + return this; + } + + /** + *

The ConversionService to use for type conversion during field binding. + * This is an alternative to {@link #conversionService(ConversionService)} + * allowing you to provide a bean name rather than a bean instance. + * + * @param conversionService the ConversionService bean name + */ + public MvcAnnotationDriven conversionService(String conversionService) { + this.conversionService = conversionService; + return this; + } + + Object conversionService() { + return this.conversionService; + } + + /** + * The HttpMessageConverter types to use for converting @RequestBody method + * parameters and @ResponseBody method return values. HttpMessageConverter + * registrations provided here will take precedence over HttpMessageConverter + * types registered by default. + * Also see {@link #shouldRegisterDefaultMessageConverters(boolean)} if + * default registrations are to be turned off altogether. + * + * @param converters the message converters + */ + public MvcAnnotationDriven messageConverters(HttpMessageConverter... converters) { + for (HttpMessageConverter converter : converters) { + this.messageConverters.add(converter); + } + return this; + } + + void messageConverters(ManagedList messageConverters) { + this.messageConverters = messageConverters; + } + + ManagedList messageConverters() { + return this.messageConverters; + } + + /** + * Indicates whether or not default HttpMessageConverter registrations should + * be added in addition to the ones provided via + * {@link #messageConverters(HttpMessageConverter...). + * + * @param shouldRegister true will result in registration of defaults. + */ + public MvcAnnotationDriven shouldRegisterDefaultMessageConverters(boolean shouldRegister) { + this.shouldRegisterDefaultMessageConverters = shouldRegister; + return this; + } + + boolean shouldRegisterDefaultMessageConverters() { + return this.shouldRegisterDefaultMessageConverters; + } + + public MvcAnnotationDriven argumentResolvers(WebArgumentResolver... resolvers) { + for (WebArgumentResolver resolver : resolvers) { + this.argumentResolvers.add(resolver); + } + return this; + } + + void argumentResolvers(ManagedList argumentResolvers) { + this.argumentResolvers = argumentResolvers; + } + + ManagedList argumentResolvers() { + return this.argumentResolvers; + } + + /** + * The Validator bean instance to use to validate Controller model objects. + * This is not required input. It only needs to be specified explicitly if + * a custom Validator needs to be configured. + * + *

If not specified, JSR-303 validation will be installed if a JSR-303 + * provider is present on the classpath. + * + * @param validator the Validator bean instance + */ + public MvcAnnotationDriven validator(Validator validator) { + this.validator = validator; + return this; + } + + /** + * The Validator bean instance to use to validate Controller model objects. + * This is an alternative to {@link #validator(Validator)} allowing you to + * provide a bean name rather than a bean instance. + * + * @param validator the Validator bean name + */ + public MvcAnnotationDriven validator(String validator) { + this.validator = validator; + return this; + } + + Object validator() { + return this.validator; + } + + /** + * The MessageCodesResolver to use to build message codes from data binding + * and validation error codes. This is not required input. If not specified + * the DefaultMessageCodesResolver is used. + * + * @param messageCodesResolver the MessageCodesResolver bean instance + */ + public MvcAnnotationDriven messageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + return this; + } + + /** + * The MessageCodesResolver to use to build message codes from data binding + * and validation error codes. This is an alternative to + * {@link #messageCodesResolver(MessageCodesResolver)} allowing you to provide + * a bean name rather than a bean instance. + * + * @param messageCodesResolver the MessageCodesResolver bean name + */ + public MvcAnnotationDriven messageCodesResolver(String messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + return this; + } + + Object messageCodesResolver() { + return this.messageCodesResolver; + } + + @Override + protected void doValidate(SimpleProblemCollector reporter) { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java new file mode 100644 index 00000000000..5e6515a6792 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcAnnotationDrivenExecutor.java @@ -0,0 +1,235 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +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.ComponentRegistrar; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.format.support.FormattingConversionServiceFactoryBean; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.ResourceHttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.util.ClassUtils; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; +import org.springframework.web.servlet.handler.MappedInterceptor; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; +import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; +import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; + +/** + * Executes {@link MvcAnnotationDriven} specifications, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + * @see MvcAnnotationDriven + */ +final class MvcAnnotationDrivenExecutor extends AbstractSpecificationExecutor { + + private static final boolean jsr303Present = ClassUtils.isPresent("javax.validation.Validator", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jacksonPresent = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) + && ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static boolean romePresent = ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", + AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + @Override + public void doExecute(MvcAnnotationDriven spec, ExecutorContext executorContext) { + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + RootBeanDefinition annMappingDef = new RootBeanDefinition(DefaultAnnotationHandlerMapping.class); + annMappingDef.setSource(source); + annMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + annMappingDef.getPropertyValues().add("order", 0); + String annMappingName = registrar.registerWithGeneratedName(annMappingDef); + + Object conversionService = getConversionService(spec, registrar); + Object validator = getValidator(spec, registrar); + Object messageCodesResolver = getMessageCodesResolver(spec, registrar); + + RootBeanDefinition bindingDef = new RootBeanDefinition(ConfigurableWebBindingInitializer.class); + bindingDef.setSource(source); + bindingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + bindingDef.getPropertyValues().add("conversionService", conversionService); + bindingDef.getPropertyValues().add("validator", validator); + bindingDef.getPropertyValues().add("messageCodesResolver", messageCodesResolver); + + ManagedList messageConverters = getMessageConverters(spec, registrar); + + RootBeanDefinition annAdapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + annAdapterDef.setSource(source); + annAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + annAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); + annAdapterDef.getPropertyValues().add("messageConverters", messageConverters); + if (!spec.argumentResolvers().isEmpty()) { + annAdapterDef.getPropertyValues().add("customArgumentResolvers", spec.argumentResolvers()); + } + String annAdapterName = registrar.registerWithGeneratedName(annAdapterDef); + + RootBeanDefinition csInterceptorDef = new RootBeanDefinition(ConversionServiceExposingInterceptor.class); + csInterceptorDef.setSource(source); + csInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, conversionService); + RootBeanDefinition mappedCsInterceptorDef = new RootBeanDefinition(MappedInterceptor.class); + mappedCsInterceptorDef.setSource(source); + mappedCsInterceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(0, (Object) null); + mappedCsInterceptorDef.getConstructorArgumentValues().addIndexedArgumentValue(1, csInterceptorDef); + String mappedInterceptorName = registrar.registerWithGeneratedName(mappedCsInterceptorDef); + + RootBeanDefinition annExceptionResolver = new RootBeanDefinition(AnnotationMethodHandlerExceptionResolver.class); + annExceptionResolver.setSource(source); + annExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + annExceptionResolver.getPropertyValues().add("messageConverters", messageConverters); + annExceptionResolver.getPropertyValues().add("order", 0); + String annExceptionResolverName = registrar.registerWithGeneratedName(annExceptionResolver); + + RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition( + ResponseStatusExceptionResolver.class); + responseStatusExceptionResolver.setSource(source); + responseStatusExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + responseStatusExceptionResolver.getPropertyValues().add("order", 1); + String responseStatusExceptionResolverName = registrar + .registerWithGeneratedName(responseStatusExceptionResolver); + + RootBeanDefinition defaultExceptionResolver = new RootBeanDefinition(DefaultHandlerExceptionResolver.class); + defaultExceptionResolver.setSource(source); + defaultExceptionResolver.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + defaultExceptionResolver.getPropertyValues().add("order", 2); + String defaultExceptionResolverName = registrar.registerWithGeneratedName(defaultExceptionResolver); + + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(spec.sourceName(), source); + compDefinition.addNestedComponent(new BeanComponentDefinition(annMappingDef, annMappingName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(annAdapterDef, annAdapterName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(annExceptionResolver, annExceptionResolverName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(responseStatusExceptionResolver, + responseStatusExceptionResolverName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(defaultExceptionResolver, + defaultExceptionResolverName)); + compDefinition.addNestedComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); + registrar.registerComponent(compDefinition); + } + + private Object getConversionService(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + if (spec.conversionService() != null) { + return getBeanOrReference(spec.conversionService()); + } else { + RootBeanDefinition conversionDef = new RootBeanDefinition(FormattingConversionServiceFactoryBean.class); + conversionDef.setSource(spec.source()); + conversionDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String conversionName = registrar.registerWithGeneratedName(conversionDef); + registrar.registerComponent(new BeanComponentDefinition(conversionDef, conversionName)); + return new RuntimeBeanReference(conversionName); + } + } + + private Object getValidator(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + if (spec.validator() != null) { + return getBeanOrReference(spec.validator()); + } else if (jsr303Present) { + RootBeanDefinition validatorDef = new RootBeanDefinition(LocalValidatorFactoryBean.class); + validatorDef.setSource(spec.source()); + validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String validatorName = registrar.registerWithGeneratedName(validatorDef); + registrar.registerComponent(new BeanComponentDefinition(validatorDef, validatorName)); + return new RuntimeBeanReference(validatorName); + } else { + return null; + } + } + + private Object getMessageCodesResolver(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + if (spec.messageCodesResolver() != null) { + return getBeanOrReference(spec.messageCodesResolver()); + } else { + return null; + } + } + + private ManagedList getMessageConverters(MvcAnnotationDriven spec, ComponentRegistrar registrar) { + ManagedList messageConverters = new ManagedList(); + Object source = spec.source(); + messageConverters.setSource(source); + messageConverters.addAll(spec.messageConverters()); + if (spec.shouldRegisterDefaultMessageConverters()) { + messageConverters.add(createConverterBeanDefinition(ByteArrayHttpMessageConverter.class, source)); + RootBeanDefinition stringConverterDef = createConverterBeanDefinition(StringHttpMessageConverter.class, + source); + stringConverterDef.getPropertyValues().add("writeAcceptCharset", false); + messageConverters.add(stringConverterDef); + messageConverters.add(createConverterBeanDefinition(ResourceHttpMessageConverter.class, source)); + messageConverters.add(createConverterBeanDefinition(SourceHttpMessageConverter.class, source)); + messageConverters.add(createConverterBeanDefinition(XmlAwareFormHttpMessageConverter.class, source)); + if (jaxb2Present) { + messageConverters + .add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); + } + if (jacksonPresent) { + messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source)); + } + if (romePresent) { + messageConverters.add(createConverterBeanDefinition(AtomFeedHttpMessageConverter.class, source)); + messageConverters.add(createConverterBeanDefinition(RssChannelHttpMessageConverter.class, source)); + } + } + return messageConverters; + } + + @SuppressWarnings("rawtypes") + private RootBeanDefinition createConverterBeanDefinition(Class converterClass, + Object source) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(converterClass); + beanDefinition.setSource(source); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + return beanDefinition; + } + + private Object getBeanOrReference(Object bean) { + if (bean != null && bean instanceof String) { + return new RuntimeBeanReference((String) bean); + } else { + return bean; + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java new file mode 100644 index 00000000000..50f96fd5148 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandler.java @@ -0,0 +1,84 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +/** + * Specifies the Spring MVC "default-servlet-handler" container feature. The + * feature provides the following fine-grained configuration: + * + *
    + *
  • {@link DefaultServletHttpRequestHandler} for serving static files by + * forwarding to the Servlet container's "default" Servlet. + *
  • {@link SimpleUrlHandlerMapping} to map the above request handler to "/**" + *
  • {@link HttpRequestHandlerAdapter} to enable the DispatcherServlet to be + * able to invoke the above request handler. + *
+ * + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcDefaultServletHandler extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcDefaultServletHandlerExecutor.class; + + private String defaultServletName; + + /** + *

Creates an instance of MvcDefaultServletHandler without. + * If this constructor is used the {@link DefaultServletHttpRequestHandler} + * will try to auto-detect the container's default Servlet at startup time + * using a list of known names. + * + *

If the default Servlet cannot be detected because of using an + * unknown container or because it has been manually configured, an + * alternate constructor provided here can be used to specify the + * servlet name explicitly. + */ + public MvcDefaultServletHandler() { + super(EXECUTOR_TYPE); + } + + /** + * The name of the default Servlet to forward to for static resource requests. + * The {@link DefaultServletHttpRequestHandler} will try to auto-detect the + * container's default Servlet at startup time using a list of known names. + * However if the default Servlet cannot be detected because of using an unknown + * container or because it has been manually configured, you can use this + * constructor to set the servlet name explicitly. + * + * @param defaultServletName the name of the default servlet + */ + public MvcDefaultServletHandler(String defaultServletName) { + this(); + this.defaultServletName = defaultServletName; + } + + String defaultServletName() { + return this.defaultServletName; + } + + @Override + protected void doValidate(SimpleProblemCollector reporter) { + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java new file mode 100644 index 00000000000..475e3a83ef8 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerExecutor.java @@ -0,0 +1,82 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config; + +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +/** + * Executes {@link MvcDefaultServletHandler} specifications, creating and + * registering bean definitions as appropriate based on the configuration + * within. + * + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 3.1 + */ +final class MvcDefaultServletHandlerExecutor extends AbstractSpecificationExecutor { + + private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; + + @Override + protected void doExecute(MvcDefaultServletHandler spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + if (!registry.containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { + RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(HttpRequestHandlerAdapter.class); + handlerAdapterDef.setSource(source); + handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); + registrar.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); + } + + RootBeanDefinition defaultServletHandlerDef = new RootBeanDefinition(DefaultServletHttpRequestHandler.class); + defaultServletHandlerDef.setSource(source); + defaultServletHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + if (StringUtils.hasText(spec.defaultServletName())) { + defaultServletHandlerDef.getPropertyValues().add("defaultServletName", spec.defaultServletName()); + } + String defaultServletHandlerName = registrar.registerWithGeneratedName(defaultServletHandlerDef); + registry.registerBeanDefinition(defaultServletHandlerName, defaultServletHandlerDef); + registrar.registerComponent(new BeanComponentDefinition(defaultServletHandlerDef, defaultServletHandlerName)); + + Map urlMap = new ManagedMap(); + urlMap.put("/**", defaultServletHandlerName); + RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); + handlerMappingDef.setSource(source); + handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + handlerMappingDef.getPropertyValues().add("urlMap", urlMap); + String handlerMappingBeanName = registrar.registerWithGeneratedName(handlerMappingDef); + registry.registerBeanDefinition(handlerMappingBeanName, handlerMappingDef); + registrar.registerComponent(new BeanComponentDefinition(handlerMappingDef, handlerMappingBeanName)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java new file mode 100644 index 00000000000..8a1ee991894 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResources.java @@ -0,0 +1,179 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.core.Ordered; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +/** + * Specifies the Spring MVC "resources" container feature. The + * feature provides the following fine-grained configuration: + * + *

    + *
  • {@link ResourceHttpRequestHandler} to serve static resources from a + * list of web-root relative, classpath, or other locations. + *
  • {@link SimpleUrlHandlerMapping} to map the above request handler to a + * a specific path pattern (e.g. "/resources/**"). + *
  • {@link HttpRequestHandlerAdapter} to enable the DispatcherServlet to be + * able to invoke the above request handler. + *
+ * + * @author Rossen Stoynchev + * @since 3.1 + */ +public final class MvcResources extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcResourcesExecutor.class; + + private Object[] locations; + + private String mapping; + + private Object cachePeriod; + + private Object order = Ordered.LOWEST_PRECEDENCE - 1; + + /** + * Create an MvcResources specification instance. See alternate constructor + * you prefer to use {@link Resource} instances instead of {@code String}-based + * resource locations. + * + * @param mapping - the URL path pattern within the current Servlet context to + * use to identify resource requests (e.g. "/resources/**"). + * @param locations - locations of resources containing static content to be + * served. Each location must point to a valid directory. Locations will be + * checked in the order specified. For example if "/" and + * "classpath:/META-INF/public-web-resources/" are configured resources will + * be served from the Web root and from any JAR on the classpath that contains + * a /META-INF/public-web-resources/ directory, with resources under the Web root + * taking precedence. + */ + public MvcResources(String mapping, String... locations) { + super(EXECUTOR_TYPE); + this.locations = locations; + this.mapping = mapping; + } + + /** + * Create an MvcResources specification instance. See alternate constructor + * defined here if you prefer to use String-based path patterns. + * + * @param mapping - the URL path pattern within the current Servlet context to + * use to identify resource requests (e.g. "/resources/**"). + * @param resources - Spring {@link Resource} objects containing static + * content to be served. Resources will be checked in the order specified. + */ + public MvcResources(String mapping, Resource... resources) { + super(EXECUTOR_TYPE); + this.locations = resources; + this.mapping = mapping; + } + + /** + * The period of time resources should be cached for in seconds. + * The default is to not send any cache headers but rather to rely on + * last-modified timestamps only. + *

Set this to 0 in order to send cache headers that prevent caching, + * or to a positive number of seconds in order to send cache headers + * with the given max-age value. + * + * @param cachePeriod the cache period in seconds + */ + public MvcResources cachePeriod(Integer cachePeriod) { + this.cachePeriod = cachePeriod; + return this; + } + + /** + * Specify a cachePeriod as a String. An alternative to {@link #cachePeriod(Integer)}. + * The String must represent an Integer after placeholder and SpEL expression + * resolution. + * + * @param cachePeriod the cache period in seconds + */ + public MvcResources cachePeriod(String cachePeriod) { + this.cachePeriod = cachePeriod; + return this; + } + + /** + * Specify a cachePeriod as a String. An alternative to {@link #cachePeriod(Integer)}. + * The String must represent an Integer after placeholder and SpEL expression + * resolution. + *

Sets the order for the SimpleUrlHandlerMapping used to match resource + * requests relative to order value for other HandlerMapping instances + * such as the {@link DefaultAnnotationHandlerMapping} used to match + * controller requests. + * + * @param order the order to use. The default value is + * {@link Ordered#LOWEST_PRECEDENCE} - 1. + */ + public MvcResources order(Integer order) { + this.order = order; + return this; + } + + /** + * Specify an order as a String. An alternative to {@link #order(Integer)}. + * The String must represent an Integer after placeholder and SpEL expression + * resolution. + * + * @param order the order to use. The default value is + * {@link Ordered#LOWEST_PRECEDENCE} - 1. + */ + public MvcResources order(String order) { + this.order = order; + return this; + } + + // Package private accessors + + Object cachePeriod() { + return cachePeriod; + } + + Object[] locations() { + return this.locations; + } + + String mapping() { + return mapping; + } + + Object order() { + return order; + } + + @Override + protected void doValidate(SimpleProblemCollector problems) { + if (!StringUtils.hasText(mapping)) { + problems.error("Mapping is required"); + } + if (locations == null || locations.length == 0) { + problems.error("At least one location is required"); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java new file mode 100644 index 00000000000..34add4cbdfb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcResourcesExecutor.java @@ -0,0 +1,85 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config; + +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +/** + * Executes {@link MvcResources} specifications, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Jeremy Grelle + * @author Rossen Stoyanchev + * @since 3.1 + */ +final class MvcResourcesExecutor extends AbstractSpecificationExecutor { + + private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter"; + + @Override + protected void doExecute(MvcResources spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + if (!registry.containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { + RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(HttpRequestHandlerAdapter.class); + handlerAdapterDef.setSource(source); + handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); + registrar.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); + } + + RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class); + resourceHandlerDef.setSource(source); + resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + resourceHandlerDef.getPropertyValues().add("locations", spec.locations()); + if (spec.cachePeriod() != null) { + resourceHandlerDef.getPropertyValues().add("cacheSeconds", spec.cachePeriod()); + } + String resourceHandlerBeanName = registrar.registerWithGeneratedName(resourceHandlerDef); + registry.registerBeanDefinition(resourceHandlerBeanName, resourceHandlerDef); + registrar.registerComponent(new BeanComponentDefinition(resourceHandlerDef, resourceHandlerBeanName)); + + Map urlMap = new ManagedMap(); + urlMap.put(spec.mapping(), resourceHandlerBeanName); + RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); + handlerMappingDef.setSource(source); + handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + handlerMappingDef.getPropertyValues().add("urlMap", urlMap); + if (spec.order() != null) { + handlerMappingDef.getPropertyValues().add("order", spec.order()); + } + String beanName = registrar.registerWithGeneratedName(handlerMappingDef); + registry.registerBeanDefinition(beanName, handlerMappingDef); + registrar.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName)); + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java new file mode 100644 index 00000000000..5ef0b9ff049 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllers.java @@ -0,0 +1,90 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.parsing.SimpleProblemCollector; +import org.springframework.context.config.AbstractFeatureSpecification; +import org.springframework.context.config.FeatureSpecificationExecutor; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; + +/** + * Specifies the Spring MVC "View Controllers" container feature. The + * feature allows specifying one or more path to view name mappings. + * It sets up the following fine-grained configuration: + * + *

    + *
  • {@link ParameterizableViewController} for each path/view name pair. + *
  • {@link SimpleUrlHandlerMapping} mapping each view controller to its path. + *
  • {@link SimpleControllerHandlerAdapter} to enable the DispatcherServlet + * to invoke the view controllers. + *
+ * + * @author Rossen Stoyanchev + * @author Chris Beams + * @since 3.1 + */ +public final class MvcViewControllers extends AbstractFeatureSpecification { + + private static final Class EXECUTOR_TYPE = MvcViewControllersExecutor.class; + + private Map mappings = new HashMap(); + + public MvcViewControllers(String path) { + this(path, null); + } + + public MvcViewControllers(String path, String viewName) { + super(EXECUTOR_TYPE); + this.mappings.put(path, viewName); + } + + public MvcViewControllers viewController(String path) { + return this.viewController(path, null); + } + + public MvcViewControllers viewController(String path, String viewName) { + this.mappings.put(path, viewName); + return this; + } + + Map mappings() { + return Collections.unmodifiableMap(mappings); + } + + @Override + protected void doValidate(SimpleProblemCollector problems) { + if (mappings.size() == 0) { + problems.error("At least one ViewController must be defined"); + } + for (String path : mappings.keySet()) { + if (!StringUtils.hasText(path)) { + problems.error("The path attribute in a ViewController is required"); + } + String viewName = mappings.get(path); + if (viewName != null && viewName.isEmpty()) { + problems.error("The view name in a ViewController may be null but not empty."); + } + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java new file mode 100644 index 00000000000..46304241ffb --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/MvcViewControllersExecutor.java @@ -0,0 +1,89 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */package org.springframework.web.servlet.config; + +import java.util.Map; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.ComponentRegistrar; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.config.AbstractSpecificationExecutor; +import org.springframework.context.config.ExecutorContext; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; + +/** + * Executes {@link MvcViewControllers} specification, creating and registering + * bean definitions as appropriate based on the configuration within. + * + * @author Keith Donald + * @author Christian Dupuis + * @author Rossen Stoyanchev + * @since 3.1 + */ +final class MvcViewControllersExecutor extends AbstractSpecificationExecutor { + + private static final String HANDLER_ADAPTER_BEAN_NAME = "org.springframework.web.servlet.config.viewControllerHandlerAdapter"; + + private static final String HANDLER_MAPPING_BEAN_NAME = "org.springframework.web.servlet.config.viewControllerHandlerMapping"; + + @Override + protected void doExecute(MvcViewControllers spec, ExecutorContext executorContext) { + BeanDefinitionRegistry registry = executorContext.getRegistry(); + ComponentRegistrar registrar = executorContext.getRegistrar(); + Object source = spec.source(); + + if (!registry.containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { + RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(SimpleControllerHandlerAdapter.class); + handlerAdapterDef.setSource(source); + handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); + registrar.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); + } + + BeanDefinition handlerMappingBeanDef = null; + if (!registry.containsBeanDefinition(HANDLER_MAPPING_BEAN_NAME)) { + RootBeanDefinition beanDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); + beanDef.setSource(source); + beanDef.getPropertyValues().add("order", "1"); + beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME, beanDef); + registrar.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_BEAN_NAME)); + handlerMappingBeanDef = beanDef; + } else { + handlerMappingBeanDef = registry.getBeanDefinition(HANDLER_MAPPING_BEAN_NAME); + } + + for (Map.Entry entry : spec.mappings().entrySet()) { + RootBeanDefinition viewControllerDef = new RootBeanDefinition(ParameterizableViewController.class); + viewControllerDef.setSource(source); + if (entry.getValue() != null) { + viewControllerDef.getPropertyValues().add("viewName", entry.getValue()); + } + if (!handlerMappingBeanDef.getPropertyValues().contains("urlMap")) { + handlerMappingBeanDef.getPropertyValues().add("urlMap", new ManagedMap()); + } + @SuppressWarnings("unchecked") + Map urlMap = (Map) handlerMappingBeanDef + .getPropertyValues().getPropertyValue("urlMap").getValue(); + urlMap.put(entry.getKey(), viewControllerDef); + } + } + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 03654ab9e5b..a054634a063 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -16,89 +16,72 @@ package org.springframework.web.servlet.config; -import java.util.Map; - -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.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.core.Ordered; +import org.springframework.context.config.ExecutorContext; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; -import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; -import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; +import org.w3c.dom.Element; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a - * {@code resources} element to register a {@link ResourceHttpRequestHandler}. - * Will also register a {@link SimpleUrlHandlerMapping} for mapping resource requests, - * and a {@link HttpRequestHandlerAdapter} if necessary. + * {@code resources} element. * - * @author Keith Donald - * @author Jeremy Grelle + * @author Rossen Stoyanchev * @since 3.0.4 + * @see MvcResources + * @see MvcResourcesExecutor */ -class ResourcesBeanDefinitionParser extends AbstractHttpRequestHandlerBeanDefinitionParser implements BeanDefinitionParser { +class ResourcesBeanDefinitionParser implements BeanDefinitionParser { - @Override - public void doParse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - registerResourceMappings(parserContext, element, source); + /** + * Parses the {@code } tag + */ + public BeanDefinition parse(Element element, ParserContext parserContext) { + MvcResources spec = createSpecification(element, parserContext); + if (spec != null) { + spec.execute(createExecutorContext(parserContext)); + } + return null; } - - private void registerResourceMappings(ParserContext parserContext, Element element, Object source) { - String resourceHandlerName = registerResourceHandler(parserContext, element, source); - if (resourceHandlerName == null) { - return; + + private MvcResources createSpecification(Element element, ParserContext parserContext) { + String mapping = element.getAttribute("mapping"); + if (!StringUtils.hasText(mapping)) { + parserContext.getReaderContext().error("The 'mapping' attribute is required.", + parserContext.extractSource(element)); + return null; } - - Map urlMap = new ManagedMap(); - String resourceRequestPath = element.getAttribute("mapping"); - if (!StringUtils.hasText(resourceRequestPath)) { - parserContext.getReaderContext().error("The 'mapping' attribute is required.", parserContext.extractSource(element)); - return; + String[] locations = StringUtils.commaDelimitedListToStringArray(element.getAttribute("location")); + if (locations.length == 0) { + parserContext.getReaderContext().error("The 'location' attribute is required.", + parserContext.extractSource(element)); + return null; } - urlMap.put(resourceRequestPath, resourceHandlerName); - - RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); - handlerMappingDef.setSource(source); - handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - - String order = element.getAttribute("order"); - // use a default of near-lowest precedence, still allowing for even lower precedence in other mappings - handlerMappingDef.getPropertyValues().add("order", StringUtils.hasText(order) ? order : Ordered.LOWEST_PRECEDENCE - 1); - - String beanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); - parserContext.getRegistry().registerBeanDefinition(beanName, handlerMappingDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName)); + MvcResources spec = new MvcResources(mapping, locations); + if (element.hasAttribute("cache-period")) { + spec.cachePeriod(element.getAttribute("cache-period")); + } + if (element.hasAttribute("order")) { + spec.order(element.getAttribute("order")); + } + spec.source(parserContext.extractSource(element)); + spec.sourceName(element.getTagName()); + return spec; } - - private String registerResourceHandler(ParserContext parserContext, Element element, Object source) { - String locationAttr = element.getAttribute("location"); - if (!StringUtils.hasText(locationAttr)) { - parserContext.getReaderContext().error("The 'location' attribute is required.", parserContext.extractSource(element)); - return null; - } - RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class); - resourceHandlerDef.setSource(source); - resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - resourceHandlerDef.getPropertyValues().add("locations", StringUtils.commaDelimitedListToStringArray(locationAttr)); - - String cacheSeconds = element.getAttribute("cache-period"); - if (StringUtils.hasText(cacheSeconds)) { - resourceHandlerDef.getPropertyValues().add("cacheSeconds", cacheSeconds); - } - - String beanName = parserContext.getReaderContext().generateBeanName(resourceHandlerDef); - parserContext.getRegistry().registerBeanDefinition(beanName, resourceHandlerDef); - parserContext.registerComponent(new BeanComponentDefinition(resourceHandlerDef, beanName)); - return beanName; + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java index 36b985e99c3..e50fae09f4d 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java @@ -16,88 +16,49 @@ package org.springframework.web.servlet.config; -import java.util.Map; - -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.ManagedMap; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.context.config.ExecutorContext; import org.springframework.web.servlet.mvc.ParameterizableViewController; -import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; +import org.w3c.dom.Element; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a * {@code view-controller} element to register a {@link ParameterizableViewController}. - * Will also register a {@link SimpleUrlHandlerMapping} for view controllers. * - * @author Keith Donald - * @author Christian Dupuis + * @author Rossen Stoyanchev * @since 3.0 + * @see MvcViewControllers + * @see MvcViewControllersExecutor */ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser { - private static final String HANDLER_ADAPTER_BEAN_NAME = - "org.springframework.web.servlet.config.viewControllerHandlerAdapter"; - - private static final String HANDLER_MAPPING_BEAN_NAME = - "org.springframework.web.servlet.config.viewControllerHandlerMapping"; - - + /** + * Parses the {@code } tag. + */ public BeanDefinition parse(Element element, ParserContext parserContext) { - Object source = parserContext.extractSource(element); - - // Register handler adapter - registerHanderAdapter(parserContext, source); - - // Register handler mapping - BeanDefinition handlerMappingDef = registerHandlerMapping(parserContext, source); - - // Create view controller bean definition - RootBeanDefinition viewControllerDef = new RootBeanDefinition(ParameterizableViewController.class); - viewControllerDef.setSource(source); - if (element.hasAttribute("view-name")) { - viewControllerDef.getPropertyValues().add("viewName", element.getAttribute("view-name")); - } - Map urlMap; - if (handlerMappingDef.getPropertyValues().contains("urlMap")) { - urlMap = (Map) handlerMappingDef.getPropertyValues().getPropertyValue("urlMap").getValue(); - } - else { - urlMap = new ManagedMap(); - handlerMappingDef.getPropertyValues().add("urlMap", urlMap); - } - urlMap.put(element.getAttribute("path"), viewControllerDef); + String path = element.getAttribute("path"); + String viewName = element.getAttribute("view-name"); + new MvcViewControllers(path, viewName.isEmpty() ? null : viewName) + .source(parserContext.extractSource(element)) + .sourceName(element.getTagName()) + .execute(createExecutorContext(parserContext)); return null; } - - private void registerHanderAdapter(ParserContext parserContext, Object source) { - if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_ADAPTER_BEAN_NAME)) { - RootBeanDefinition handlerAdapterDef = new RootBeanDefinition(SimpleControllerHandlerAdapter.class); - handlerAdapterDef.setSource(source); - handlerAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - parserContext.getRegistry().registerBeanDefinition(HANDLER_ADAPTER_BEAN_NAME, handlerAdapterDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME)); - } - } - - private BeanDefinition registerHandlerMapping(ParserContext parserContext, Object source) { - if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_MAPPING_BEAN_NAME)) { - RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); - handlerMappingDef.setSource(source); - handlerMappingDef.getPropertyValues().add("order", "1"); - handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - parserContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME, handlerMappingDef); - parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME)); - return handlerMappingDef; - } - else { - return parserContext.getRegistry().getBeanDefinition(HANDLER_MAPPING_BEAN_NAME); - } + + /** + * Adapt the given ParserContext instance into an ExecutorContext. + * + * TODO SPR-7420: consider unifying the two through a superinterface. + * TODO SPR-7420: create a common ParserContext-to-ExecutorContext adapter util + */ + private ExecutorContext createExecutorContext(ParserContext parserContext) { + ExecutorContext executorContext = new ExecutorContext(); + executorContext.setRegistry(parserContext.getRegistry()); + executorContext.setRegistrar(parserContext); + executorContext.setProblemReporter(parserContext.getReaderContext().getProblemReporter()); + return executorContext; } } diff --git a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd index 7a53516a12e..b788c933144 100644 --- a/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd +++ b/org.springframework.web.servlet/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.1.xsd @@ -23,7 +23,8 @@ @@ -36,6 +37,13 @@ + + + + + diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java index 11f60904e8c..1c2be54e33a 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -64,8 +64,15 @@ public class AnnotationDrivenBeanDefinitionParserTests { @Test public void testMessageConverters() { loadBeanDefinitions("mvc-config-message-converters.xml"); - verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerAdapter.class)); - verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class)); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerAdapter.class), true); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class), true); + } + + @Test + public void testMessageConvertersWithoutDefaultRegistrations() { + loadBeanDefinitions("mvc-config-message-converters-defaults-off.xml"); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerAdapter.class), false); + verifyMessageConverters(appContext.getBean(AnnotationMethodHandlerExceptionResolver.class), false); } @Test @@ -88,12 +95,18 @@ public class AnnotationDrivenBeanDefinitionParserTests { appContext.refresh(); } - private void verifyMessageConverters(Object bean) { + private void verifyMessageConverters(Object bean, boolean hasDefaultRegistrations) { assertNotNull(bean); Object converters = new DirectFieldAccessor(bean).getPropertyValue("messageConverters"); assertNotNull(converters); assertTrue(converters instanceof HttpMessageConverter[]); - assertEquals(2, ((HttpMessageConverter[]) converters).length); + if (hasDefaultRegistrations) { + assertTrue("Default converters are registered in addition to custom ones", + ((HttpMessageConverter[]) converters).length > 2); + } else { + assertTrue("Default converters should not be registered", + ((HttpMessageConverter[]) converters).length == 2); + } assertTrue(((HttpMessageConverter[]) converters)[0] instanceof StringHttpMessageConverter); assertTrue(((HttpMessageConverter[]) converters)[1] instanceof ResourceHttpMessageConverter); } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.java new file mode 100644 index 00000000000..a1f3d633cca --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcAnnotationDrivenFeatureTests.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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; + +/** + * Integration tests for the {@link MvcAnnotationDriven} feature specification. + * @author Rossen Stoyanchev + * @author Chris Beams + * @since 3.1 + */ +public class MvcAnnotationDrivenFeatureTests { + + @Test + public void testMessageCodesResolver() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcFeature.class, MvcBeans.class); + ctx.refresh(); + AnnotationMethodHandlerAdapter adapter = ctx.getBean(AnnotationMethodHandlerAdapter.class); + assertNotNull(adapter); + Object initializer = new DirectFieldAccessor(adapter).getPropertyValue("webBindingInitializer"); + assertNotNull(initializer); + MessageCodesResolver resolver = ((ConfigurableWebBindingInitializer) initializer).getMessageCodesResolver(); + assertNotNull(resolver); + assertEquals("test.foo.bar", resolver.resolveMessageCodes("foo", "bar")[0]); + Object argResolvers = new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers"); + assertNotNull(argResolvers); + WebArgumentResolver[] argResolversArray = (WebArgumentResolver[]) argResolvers; + assertEquals(1, argResolversArray.length); + assertTrue(argResolversArray[0] instanceof TestWebArgumentResolver); + Object converters = new DirectFieldAccessor(adapter).getPropertyValue("messageConverters"); + assertNotNull(converters); + HttpMessageConverter[] convertersArray = (HttpMessageConverter[]) converters; + assertTrue("Default converters are registered in addition to the custom one", convertersArray.length > 1); + assertTrue(convertersArray[0] instanceof StringHttpMessageConverter); + } + +} + +@FeatureConfiguration +class MvcFeature { + @Feature + public MvcAnnotationDriven annotationDriven(MvcBeans mvcBeans) { + return new MvcAnnotationDriven() + .conversionService(mvcBeans.conversionService()) + .messageCodesResolver(mvcBeans.messageCodesResolver()) + .validator(mvcBeans.validator()) + .messageConverters(new StringHttpMessageConverter()) + .argumentResolvers(new TestWebArgumentResolver()); + } +} + +@Configuration +class MvcBeans { + @Bean + public FormattingConversionService conversionService() { + return new DefaultFormattingConversionService(); + } + @Bean + public Validator validator() { + return new LocalValidatorFactoryBean(); + } + @Bean MessageCodesResolver messageCodesResolver() { + return new TestMessageCodesResolver(); + } +} + diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java new file mode 100644 index 00000000000..74317c6bb88 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcDefaultServletHandlerTests.java @@ -0,0 +1,61 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; + +/** + * Test fixture for {@link MvcDefaultServletHandler} feature specification. + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcDefaultServletHandlerTests { + + @Test + public void testDefaultServletHandler() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcDefaultServletHandlerFeature.class); + ctx.refresh(); + HttpRequestHandlerAdapter adapter = ctx.getBean(HttpRequestHandlerAdapter.class); + assertNotNull(adapter); + DefaultServletHttpRequestHandler handler = ctx.getBean(DefaultServletHttpRequestHandler.class); + assertNotNull(handler); + String defaultServletHandlerName = (String) new DirectFieldAccessor(handler) + .getPropertyValue("defaultServletName"); + assertEquals("foo", defaultServletHandlerName); + } + + @FeatureConfiguration + private static class MvcDefaultServletHandlerFeature { + + @SuppressWarnings("unused") + @Feature + public MvcDefaultServletHandler defaultServletHandler() { + return new MvcDefaultServletHandler("foo"); + } + + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java new file mode 100644 index 00000000000..bf71da49afa --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcResourcesTests.java @@ -0,0 +1,98 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Test; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; +import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; + +/** + * Test fixture for {@link MvcResources} feature specification. + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcResourcesTests { + + @Test + public void testResources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcResourcesFeature.class); + ctx.refresh(); + HttpRequestHandlerAdapter adapter = ctx.getBean(HttpRequestHandlerAdapter.class); + assertNotNull(adapter); + ResourceHttpRequestHandler handler = ctx.getBean(ResourceHttpRequestHandler.class); + assertNotNull(handler); + @SuppressWarnings("unchecked") + List locations = (List) new DirectFieldAccessor(handler).getPropertyValue("locations"); + assertNotNull(locations); + assertEquals(2, locations.size()); + assertEquals("foo", locations.get(0).getFilename()); + assertEquals("bar", locations.get(1).getFilename()); + SimpleUrlHandlerMapping mapping = ctx.getBean(SimpleUrlHandlerMapping.class); + assertEquals(1, mapping.getOrder()); + assertSame(handler, mapping.getHandlerMap().get("/resources/**")); + } + + @Test + public void testInvalidResources() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(InvalidMvcResourcesFeature.class); + try { + ctx.refresh(); + fail("Invalid feature spec should not validate"); + } catch (RuntimeException e) { + assertTrue(e.getCause().getMessage().contains("Mapping is required")); + // TODO : should all problems be in the message ? + } + } + + @FeatureConfiguration + private static class MvcResourcesFeature { + + @SuppressWarnings("unused") + @Feature + public MvcResources resources() { + return new MvcResources("/resources/**", new String[] { "/foo", "/bar" }).cachePeriod(86400).order(1); + } + + } + + @FeatureConfiguration + private static class InvalidMvcResourcesFeature { + + @SuppressWarnings("unused") + @Feature + public MvcResources resources() { + return new MvcResources(" ", new String[] {}); + } + + } + +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java new file mode 100644 index 00000000000..fdb27d71557 --- /dev/null +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/MvcViewControllersTests.java @@ -0,0 +1,131 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.config; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.beans.factory.parsing.FailFastProblemReporter; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Feature; +import org.springframework.context.annotation.FeatureConfiguration; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; + +/** + * Test fixture for {@link MvcViewControllers} feature specification. + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class MvcViewControllersTests { + + @Test + public void testMvcViewControllers() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(MvcViewControllersFeature.class); + ctx.refresh(); + SimpleControllerHandlerAdapter adapter = ctx.getBean(SimpleControllerHandlerAdapter.class); + assertNotNull(adapter); + SimpleUrlHandlerMapping handler = ctx.getBean(SimpleUrlHandlerMapping.class); + assertNotNull(handler); + Map urlMap = handler.getUrlMap(); + assertNotNull(urlMap); + assertEquals(2, urlMap.size()); + ParameterizableViewController controller = (ParameterizableViewController) urlMap.get("/"); + assertNotNull(controller); + assertEquals("home", controller.getViewName()); + controller = (ParameterizableViewController) urlMap.get("/account"); + assertNotNull(controller); + assertNull(controller.getViewName()); + } + + @Test + public void testEmptyPath() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EmptyPathViewControllersFeature.class); + try { + ctx.refresh(); + fail("expected exception"); + } catch (Exception ex) { + assertTrue(ex.getCause().getMessage().contains("path attribute")); + } + } + + @Test + public void testEmptyViewName() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(EmptyViewNameViewControllersFeature.class); + try { + ctx.refresh(); + fail("expected exception"); + } catch (Exception ex) { + assertTrue(ex.getCause().getMessage().contains("not empty")); + } + } + + @Test + public void testNullViewName() { + FailFastProblemReporter problemReporter = new FailFastProblemReporter(); + assertThat(new MvcViewControllers("/some/path").validate(problemReporter), is(true)); + } + + + @FeatureConfiguration + private static class MvcViewControllersFeature { + + @SuppressWarnings("unused") + @Feature + public MvcViewControllers mvcViewControllers() { + return new MvcViewControllers("/", "home").viewController("/account"); + } + + } + + + @FeatureConfiguration + private static class EmptyViewNameViewControllersFeature { + + @SuppressWarnings("unused") + @Feature + public MvcViewControllers mvcViewControllers() { + return new MvcViewControllers("/some/path", ""); + } + + } + + + @FeatureConfiguration + private static class EmptyPathViewControllersFeature { + + @SuppressWarnings("unused") + @Feature + public MvcViewControllers mvcViewControllers() { + return new MvcViewControllers("", "someViewName"); + } + + } + +} + diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java index 117f5d70b0c..93a18beb330 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7766Tests.java @@ -1,10 +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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.servlet.mvc.annotation; import java.awt.Color; import org.junit.Test; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -16,7 +32,7 @@ public class Spr7766Tests { public void test() throws Exception { AnnotationMethodHandlerAdapter adapter = new AnnotationMethodHandlerAdapter(); ConfigurableWebBindingInitializer binder = new ConfigurableWebBindingInitializer(); - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); service.addConverter(new ColorConverter()); binder.setConversionService(service); adapter.setWebBindingInitializer(binder); diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java index 5ed4d0b3f29..26012a43790 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/Spr7839Tests.java @@ -1,3 +1,19 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.web.servlet.mvc.annotation; import static org.junit.Assert.assertEquals; @@ -9,7 +25,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -30,7 +46,7 @@ public class Spr7839Tests { @Before public void setUp() { ConfigurableWebBindingInitializer binder = new ConfigurableWebBindingInitializer(); - GenericConversionService service = ConversionServiceFactory.createDefaultConversionService(); + GenericConversionService service = new DefaultConversionService(); service.addConverter(new Converter() { public NestedBean convert(String source) { return new NestedBean(source); diff --git a/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml new file mode 100644 index 00000000000..22344c79966 --- /dev/null +++ b/org.springframework.web.servlet/src/test/resources/org/springframework/web/servlet/config/mvc-config-message-converters-defaults-off.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java b/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java index dab8d8ed78a..ffd07d479f2 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -16,6 +16,12 @@ package org.springframework.http; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; @@ -23,11 +29,9 @@ import java.util.Comparator; import java.util.List; import java.util.Random; -import static org.junit.Assert.*; import org.junit.Test; - import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.DefaultConversionService; /** * @author Arjen Poutsma @@ -490,7 +494,7 @@ public class MediaTypeTests { @Test public void testWithConversionService() { - ConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + ConversionService conversionService = new DefaultConversionService(); assertTrue(conversionService.canConvert(String.class, MediaType.class)); MediaType mediaType = MediaType.parseMediaType("application/xml"); assertEquals(mediaType, conversionService.convert("application/xml", MediaType.class)); diff --git a/spring-framework-reference/src/mvc.xml b/spring-framework-reference/src/mvc.xml index c49e1ef2248..c9e983ce74e 100644 --- a/spring-framework-reference/src/mvc.xml +++ b/spring-framework-reference/src/mvc.xml @@ -3368,8 +3368,10 @@ public class SimpleController { - This list of HttpMessageConverters used can be replaced through - the mvc:message-converters sub-element of mvc:annotation-driven. + You can provide your own HttpMessageConverters through the + mvc:message-converters sub-element of mvc:annotation-driven. + Message converters you provide will take precedence over the + ones registered by default.