From e48fe0d89271113bcac4e7c41e86cb1d8230594e Mon Sep 17 00:00:00 2001 From: Chris Beams Date: Fri, 27 Feb 2009 01:35:44 +0000 Subject: [PATCH] Initial cut at migrating JavaConfig essentials into Core. test.basic.BasicTests successfully excercises the simplest possible @Configuration class (contains a single @Bean method). 39 classes were pulled in from from JavaConfig to support this use case. --- .../org.eclipse.wst.common.component | 9 + ....eclipse.wst.common.project.facet.core.xml | 7 + .../org.eclipse.wst.validation.prefs | 6 + org.springframework.config.java/build.xml | 12 + org.springframework.config.java/ivy.xml | 50 +++ .../org/springframework/config/java/Util.java | 164 +++++++++ .../annotation/AbstractMethodInterceptor.java | 48 +++ .../config/java/annotation/Bean.java | 214 +++++++++++ .../annotation/BeanDefinitionRegistrar.java | 36 ++ .../annotation/BeanMethodInterceptor.java | 84 +++++ .../config/java/annotation/BeanRegistrar.java | 201 +++++++++++ .../config/java/annotation/Configuration.java | 97 +++++ .../config/java/annotation/Factory.java | 55 +++ .../config/java/annotation/Validator.java | 14 + .../enhancement/AddAnnotationAdapter.java | 114 ++++++ .../CglibConfigurationEnhancer.java | 237 ++++++++++++ .../enhancement/ConfigurationEnhancer.java | 24 ++ .../enhancement/InitializingBeanCallback.java | 47 +++ .../InitializingBeanRegistrar.java | 33 ++ .../AsmJavaConfigBeanDefinitionReader.java | 83 +++++ ...onfigurationModelBeanDefinitionReader.java | 189 ++++++++++ .../internal/parsing/ConfigurationParser.java | 60 ++++ .../parsing/asm/AnnotationAdapter.java | 66 ++++ .../parsing/asm/AsmConfigurationParser.java | 88 +++++ .../java/internal/parsing/asm/AsmUtils.java | 137 +++++++ .../asm/ConfigurationClassMethodVisitor.java | 146 ++++++++ .../asm/ConfigurationClassVisitor.java | 234 ++++++++++++ .../parsing/asm/MutableAnnotation.java | 36 ++ .../asm/MutableAnnotationArrayVisitor.java | 71 ++++ .../MutableAnnotationInvocationHandler.java | 202 +++++++++++ .../parsing/asm/MutableAnnotationUtils.java | 44 +++ .../parsing/asm/MutableAnnotationVisitor.java | 115 ++++++ .../InternalConfigurationPostProcessor.java | 150 ++++++++ .../config/java/model/ConfigurationClass.java | 338 ++++++++++++++++++ .../config/java/model/ConfigurationModel.java | 176 +++++++++ .../MalformedJavaConfigurationException.java | 75 ++++ .../config/java/model/ModelClass.java | 160 +++++++++ .../config/java/model/ModelMethod.java | 243 +++++++++++++ .../config/java/model/UsageError.java | 73 ++++ .../config/java/model/Validatable.java | 19 + .../config/java/plugin/Extension.java | 48 +++ ...sionAnnotationBeanDefinitionRegistrar.java | 32 ++ .../process/ConfigurationPostProcessor.java | 50 +++ .../config/java/util/DefaultScopes.java | 37 ++ .../src/test/java/test/basic/BasicTests.java | 110 ++++++ .../src/test/java/test/beans/Colour.java | 38 ++ .../java/test/beans/DependsOnTestBean.java | 31 ++ .../test/java/test/beans/INestedTestBean.java | 24 ++ .../src/test/java/test/beans/IOther.java | 24 ++ .../src/test/java/test/beans/ITestBean.java | 62 ++++ .../test/java/test/beans/IndexedTestBean.java | 115 ++++++ .../test/java/test/beans/NestedTestBean.java | 62 ++++ .../src/test/java/test/beans/TestBean.java | 295 +++++++++++++++ org.springframework.config.java/template.mf | 24 ++ 54 files changed, 5109 insertions(+) create mode 100644 org.springframework.config.java/.settings/org.eclipse.wst.common.component create mode 100644 org.springframework.config.java/.settings/org.eclipse.wst.common.project.facet.core.xml create mode 100644 org.springframework.config.java/.settings/org.eclipse.wst.validation.prefs create mode 100644 org.springframework.config.java/build.xml create mode 100644 org.springframework.config.java/ivy.xml create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/Util.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/AbstractMethodInterceptor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Bean.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanDefinitionRegistrar.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanMethodInterceptor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanRegistrar.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Configuration.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Factory.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Validator.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/AddAnnotationAdapter.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/CglibConfigurationEnhancer.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/ConfigurationEnhancer.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanCallback.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanRegistrar.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/AsmJavaConfigBeanDefinitionReader.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/ConfigurationModelBeanDefinitionReader.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/ConfigurationParser.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AnnotationAdapter.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmConfigurationParser.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmUtils.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassMethodVisitor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassVisitor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotation.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationArrayVisitor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationInvocationHandler.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationUtils.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationVisitor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/internal/process/InternalConfigurationPostProcessor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationClass.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationModel.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/MalformedJavaConfigurationException.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelClass.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelMethod.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/UsageError.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/model/Validatable.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/Extension.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/ExtensionAnnotationBeanDefinitionRegistrar.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/process/ConfigurationPostProcessor.java create mode 100644 org.springframework.config.java/src/main/java/org/springframework/config/java/util/DefaultScopes.java create mode 100644 org.springframework.config.java/src/test/java/test/basic/BasicTests.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/Colour.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/DependsOnTestBean.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/INestedTestBean.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/IOther.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/ITestBean.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/IndexedTestBean.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/NestedTestBean.java create mode 100644 org.springframework.config.java/src/test/java/test/beans/TestBean.java create mode 100644 org.springframework.config.java/template.mf diff --git a/org.springframework.config.java/.settings/org.eclipse.wst.common.component b/org.springframework.config.java/.settings/org.eclipse.wst.common.component new file mode 100644 index 0000000000..5e5e630590 --- /dev/null +++ b/org.springframework.config.java/.settings/org.eclipse.wst.common.component @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.springframework.config.java/.settings/org.eclipse.wst.common.project.facet.core.xml b/org.springframework.config.java/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100644 index 0000000000..f134fda3be --- /dev/null +++ b/org.springframework.config.java/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.springframework.config.java/.settings/org.eclipse.wst.validation.prefs b/org.springframework.config.java/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 0000000000..ae066546e8 --- /dev/null +++ b/org.springframework.config.java/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Fri Apr 13 18:31:13 EEST 2007 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/org.springframework.config.java/build.xml b/org.springframework.config.java/build.xml new file mode 100644 index 0000000000..067ae320e6 --- /dev/null +++ b/org.springframework.config.java/build.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/org.springframework.config.java/ivy.xml b/org.springframework.config.java/ivy.xml new file mode 100644 index 0000000000..eb18a2ee5b --- /dev/null +++ b/org.springframework.config.java/ivy.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/Util.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/Util.java new file mode 100644 index 0000000000..499f318aa9 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/Util.java @@ -0,0 +1,164 @@ +package org.springframework.config.java; + +import static java.lang.String.*; +import static org.springframework.util.ClassUtils.*; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.lang.reflect.Constructor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import sun.security.x509.Extension; + +/** + * Misc utils + * + * @author Chris Beams + */ +// TODO: SJC-242 general - check cycles with s101 +// TODO: SJC-242 general - check tabs & spaces +// TODO: SJC-242 general - check apache header +// TODO: SJC-242 rename, repackage, document +public class Util { + + private static final Log log = LogFactory.getLog(Util.class); + + private Util() { } + + /** + * Returns instance of type T by invoking its default or no-arg + * constructor. + *

+ * Any reflection-related issues are re-thrown as unchecked. + */ + public static T getInstance(Class clazz) { + try { + Constructor noArgCtor = clazz.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(noArgCtor); + return noArgCtor.newInstance(); + } catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + throw new IllegalStateException( + format("Unexpected reflection exception - %s: %s", + ex.getClass().getName(), ex.getMessage())); + } + } + + /** + * Loads the specified class using the default class loader, gracefully handling any + * {@link ClassNotFoundException} that may be thrown. This functionality is specifically + * implemented to accomodate tooling (Spring IDE) concerns, where user-defined types will not be + * + * @param type of class to be returned + * @param fqClassName fully-qualified class name + * + * @return newly loaded class instance, null if class could not be found + * + * @see #loadRequiredClass(String) + * @see #loadToolingSafeClass(String) + * @see ClassUtils#getDefaultClassLoader() + */ + @SuppressWarnings("unchecked") + public static Class loadClass(String fqClassName) { + try { + return (Class) ClassUtils.getDefaultClassLoader().loadClass(fqClassName); + } catch (ClassNotFoundException ex) { + return null; + } + } + + /** + * Loads the specified class using the default class loader, rethrowing any + * {@link ClassNotFoundException} as an unchecked exception. + * + * @param type of class to be returned + * @param fqClassName fully-qualified class name + * + * @return newly loaded class instance + * + * @throws IllegalArgumentException if configClassName cannot be loaded. + * + * @see #loadClass(String) + * @see #loadToolingSafeClass(String) + * @see ClassUtils#getDefaultClassLoader() + */ + @SuppressWarnings("unchecked") + public static Class loadRequiredClass(String fqClassName) { + try { + return (Class)getDefaultClassLoader().loadClass(fqClassName); + } catch (ClassNotFoundException ex) { + throw new IllegalArgumentException( + format("Class [%s] could not be loaded, check your CLASSPATH.", fqClassName), ex); + } + } + + /** + * Loads the specified class using the default class loader, gracefully handling any + * {@link ClassNotFoundException} that may be thrown by issuing a WARN level logging statement + * and return null. This functionality is specifically implemented to accomodate tooling + * (Spring IDE) concerns, where user-defined types will not be available to the tooling. + *

+ * ASM class reading is used throughout JavaConfig, but there are certain cases where + * classloading cannot be avoided - specifically in cases where users define their own + * {@link Extension} or {@link Factory} annotations. This method should therefore be used sparingly + * but consistently where required. + *

+ * Because {@link ClassNotFoundException} is compensated for by returning null, callers must + * take care to handle the null case appropriately. + *

+ * In cases where the WARN logging statement is not desired, use the {@link #loadClass(String)} + * method, which returns null but issues no logging statements. + *

+ * This method should only ever return null in the case of a user-defined type be processed at + * tooling time. Therefore, tooling may not be able to represent any custom annotation + * semantics, but JavaConfig itself will not have any problem loading and respecting them at + * actual runtime. + * + * @param type of class to be returned + * @param fqClassName fully-qualified class name + * + * @return newly loaded class, null if class could not be found. + * + * @see #loadClass(String) + * @see #loadRequiredClass(String) + * @see ClassUtils#getDefaultClassLoader() + */ + @SuppressWarnings("unchecked") + public static Class loadToolingSafeClass(String fqClassName) { + try { + return (Class) ClassUtils.getDefaultClassLoader().loadClass(fqClassName); + } catch (ClassNotFoundException ex) { + log.warn(format("Unable to load class [%s], likely due to tooling-specific restrictions." + + "Attempting to continue, but unexpected errors may occur", fqClassName), ex); + return null; + } + } + + /** + * Uses the default ClassLoader to load pathToClass. Appends '.class' + * to pathToClass before attempting to load. + * + * @param pathToClass resource path to class, not including .class suffix. + * e.g.: com/acme/MyClass + * + * @return inputStream for pathToClass + * + * @throws RuntimeException if pathToClass does not exist + */ + public static InputStream getClassAsStream(String pathToClass) { + String classFileName = pathToClass + ClassUtils.CLASS_FILE_SUFFIX; + + InputStream is = ClassUtils.getDefaultClassLoader().getResourceAsStream(classFileName); + + if (is == null) + throw new RuntimeException( + new FileNotFoundException("Class file [" + classFileName + "] not found")); + + return is; + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/AbstractMethodInterceptor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/AbstractMethodInterceptor.java new file mode 100644 index 0000000000..bdf615860e --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/AbstractMethodInterceptor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.annotation; + +import java.lang.reflect.Method; + +import net.sf.cglib.proxy.MethodInterceptor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.util.Assert; + + +/** + * Base class for all {@link MethodInterceptor} implementations. + * + * @author Chris Beams + */ +abstract class AbstractMethodInterceptor implements BeanFactoryAware, MethodInterceptor { + protected final Log log = LogFactory.getLog(this.getClass()); + protected DefaultListableBeanFactory beanFactory; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isInstanceOf(DefaultListableBeanFactory.class, beanFactory); + this.beanFactory = (DefaultListableBeanFactory) beanFactory; + } + + protected String getBeanName(Method method) { + return method.getName(); + } +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Bean.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Bean.java new file mode 100644 index 0000000000..d7f7b12fc6 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Bean.java @@ -0,0 +1,214 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.config.java.model.ModelMethod; +import org.springframework.config.java.model.UsageError; + + +/** + * Annotation to be applied to methods that create beans in a Spring context. The name of the bean + * is the method name. (It is also possible to specify aliases using the aliases array on this + * annotation.) + * + *

Contains information similar to that held in Spring's internal BeanDefinition metadata.

+ * + *

Bean creation methods must be non-private (default, public or protected). Bean creation + * methods may throw any exception, which will be caught and handled by the Spring container on + * processing of the configuration class.
+ * Bean creation methods must return an object type. The decision to return a class or an interface + * will be significant in the event of proxying. Bean methods that return interfaces will be proxied + * using dynamic proxies; those that return a class will require CGLIB or other subclass-based + * proxying. It is recommended to return an interface where possible, as this is also consistent + * with best practice around loose coupling.

+ * + *

Bean creation methods may reference other bean creation methods by calling them directly, as + * follows. This ensures that references between beans are strongly typed:

+ * + * @see Configuration + * @see BeanNamingStrategy + * + * @author Rod Johnson + * @author Costin Leau + * @author Chris Beams + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Factory(registrarType=BeanRegistrar.class, + callbackType=BeanMethodInterceptor.class, + validatorTypes={BeanValidator.class, + IllegalBeanOverrideValidator.class}) +public @interface Bean { + + /** + * Role this bean plays in the overall application configuration. + * + * @see BeanDefinition#ROLE_APPLICATION + * @see BeanDefinition#ROLE_INFRASTRUCTURE + * @see BeanDefinition#ROLE_SUPPORT + * + * @see AbstractBeanDefinition the 'role' field is assigned by default to ROLE_APPLICATION + */ + int role() default BeanDefinition.ROLE_APPLICATION; + + /** + * Bean aliases. + */ + String[] aliases() default { }; + + /** + * Scope: whether the bean is a singleton, prototype or custom scope. + * Default is singleton. + */ + String scope() default BeanDefinition.SCOPE_SINGLETON; + + /** + * Bean autowire strategy. + */ + Autowire autowire() default Autowire.INHERITED; + +// /** +// * Bean lazy strategy. +// */ +// Lazy lazy() default Lazy.UNSPECIFIED; +// +// /** +// * A bean may be marked as primary, useful for disambiguation when looking +// * up beans by type. +// * +// * @see org.springframework.config.java.context.JavaConfigApplicationContext#getBean(Class); +// */ +// Primary primary() default Primary.UNSPECIFIED; + + /** + * Bean init method name. Normally this is not needed, as the initialization + * (with parameterization) can be done directly through java code. + */ + String initMethodName() default ""; + + /** + * Bean destroy method name. + */ + String destroyMethodName() default ""; + +// /** +// * Bean dependency check strategy. +// */ +// DependencyCheck dependencyCheck() default DependencyCheck.UNSPECIFIED; + + /** + * Beans on which the current bean depends on. + */ + String[] dependsOn() default { }; + +// /** +// * Metadata for the current bean. +// */ +// Meta[] meta() default { }; + + /** + * Allow the bean to be overridden in another JavaConfig, XML or other + * non-Java configuration. This is consistent with + * DefaultListableBeanFactory's allowBeanDefinitionOverriding property, + * which defaults to true. + * + * @return whether overriding of this bean is allowed + */ + boolean allowOverriding() default true; + +} + + +/** + * Detects any user errors when declaring {@link Bean}-annotated methods. + * + * @author Chris Beams + */ +class BeanValidator implements Validator { + + public boolean supports(Object object) { + return object instanceof ModelMethod; + } + + public void validate(Object object, List errors) { + ModelMethod method = (ModelMethod) object; + + // TODO: re-enable for @ScopedProxy support +// if (method.getAnnotation(ScopedProxy.class) == null) +// return; +// +// Bean bean = method.getRequiredAnnotation(Bean.class); +// +// if (bean.scope().equals(DefaultScopes.SINGLETON) +// || bean.scope().equals(DefaultScopes.PROTOTYPE)) +// errors.add(new InvalidScopedProxyDeclarationError(method)); + } + +} + + +/** + * Detects any illegally-overridden {@link Bean} definitions within a particular + * {@link ConfigurationModel} + * + * @see Bean#allowOverriding() + * + * @author Chris Beams + */ +class IllegalBeanOverrideValidator implements Validator { + + public boolean supports(Object object) { + return object instanceof ConfigurationModel; + } + + public void validate(Object object, List errors) { + ConfigurationModel model = (ConfigurationModel) object; + + ConfigurationClass[] allClasses = model.getAllConfigurationClasses(); + + for (int i = 0; i < allClasses.length; i++) { + for (ModelMethod method : allClasses[i].getMethods()) { + Bean bean = method.getAnnotation(Bean.class); + + if (bean == null || bean.allowOverriding()) + continue; + + for (int j = i + 1; j < allClasses.length; j++) + if (allClasses[j].hasMethod(method.getName())) + errors.add(allClasses[i].new IllegalBeanOverrideError(allClasses[j], method)); + } + } + } + +} + + + diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanDefinitionRegistrar.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanDefinitionRegistrar.java new file mode 100644 index 0000000000..40f8402dd0 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanDefinitionRegistrar.java @@ -0,0 +1,36 @@ +package org.springframework.config.java.annotation; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.config.java.model.ModelMethod; + +/** + * Registers bean definition(s) for a particular method, usually based on its annotation metadata. + * + *

Constraints

+ * Implementations must have only a default constructor, or explicitly declare + * a no-arg constructor. + * + * @see Factory + * @see ModelMethod + * + * @author Chris Beams + */ +// TODO: SJC-242 document FactoryMethodHandler +// TODO: SJC-242 odd that the api here uses both ModelMethod and java.lang.reflect.Member +// TODO: SJC-242 document that there must be a no-arg ctor +public interface BeanDefinitionRegistrar { + + /** + * Determines whether this registrar is capable of handling method. + */ + // TODO: rename to supports() in alignment with Validator nomenclature + boolean accepts(Method method); + + /** + * Registers any bean definitions for method with registry. + */ + void register(ModelMethod method, BeanDefinitionRegistry registry); + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanMethodInterceptor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanMethodInterceptor.java new file mode 100644 index 0000000000..4ee4be5c61 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanMethodInterceptor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.annotation; + +import static java.lang.String.*; + +import java.lang.reflect.Method; + +import net.sf.cglib.proxy.MethodProxy; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; + +/** + * Intercepts the invocation of any {@link Bean}-annotated methods in order to ensure proper + * handling of bean semantics such as scoping and AOP proxying. + * + * @see Bean + * @see BeanRegistrar + * + * @author Chris Beams + */ +class BeanMethodInterceptor extends AbstractMethodInterceptor { + + /** + * Enhances a {@link Bean @Bean} method to check the supplied BeanFactory for the existence + * of this bean object. + */ + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + String beanName = getBeanName(method); + + // TODO: re-enable for @ScopedProxy support +// boolean isScopedProxy = (AnnotationUtils.findAnnotation(method, ScopedProxy.class) != null); +// +// String scopedBeanName = ScopedProxy.Util.resolveHiddenScopedProxyBeanName(beanName); +// if (isScopedProxy && beanFactory.isCurrentlyInCreation(scopedBeanName)) +// beanName = scopedBeanName; + + if (factoryContainsBean(beanName)) { + // we have an already existing cached instance of this bean -> retrieve it + Object cachedBean = beanFactory.getBean(beanName); + if (log.isInfoEnabled()) + log.info(format("Returning cached singleton object [%s] for @Bean method %s.%s", + cachedBean, method.getDeclaringClass().getSimpleName(), beanName)); + + return cachedBean; + } + + return proxy.invokeSuper(obj, args); + } + + /** + * Check the beanFactory to see whether the bean named beanName already exists. + * Accounts for the fact that the requested bean may be "in creation", i.e.: we're in the + * middle of servicing the initial request for this bean. From JavaConfig's perspective, + * this means that the bean does not actually yet exist, and that it is now our job to + * create it for the first time by executing the logic in the corresponding Bean method. + *

+ * Said another way, this check repurposes {@link ConfigurableBeanFactory#isCurrentlyInCreation(String)} + * to determine whether the container is calling this method or the user is calling this method. + * + * @param beanName name of bean to check for + * + * @return true if beanName already exists in beanFactory + */ + private boolean factoryContainsBean(String beanName) { + return beanFactory.containsBean(beanName) + && !beanFactory.isCurrentlyInCreation(beanName); + } + + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanRegistrar.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanRegistrar.java new file mode 100644 index 0000000000..1b583976b6 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/BeanRegistrar.java @@ -0,0 +1,201 @@ +package org.springframework.config.java.annotation; + +import static java.lang.String.*; +import static org.springframework.util.StringUtils.*; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.MalformedJavaConfigurationException; +import org.springframework.config.java.model.ModelMethod; +import org.springframework.config.java.model.UsageError; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; + + +// TODO: SJC-242 document BeanHandler +// TODO: SJC-242 make package-private +class BeanRegistrar implements BeanDefinitionRegistrar { + + private static final Log logger = LogFactory.getLog(BeanRegistrar.class); + + /** + * Ensures that member is a method and is annotated (directly or indirectly) + * with {@link Bean @Bean}. + */ + public boolean accepts(Method method) { + return AnnotationUtils.findAnnotation(method, Bean.class) != null; + } + + // TODO: SJC-242 method too long + public void register(ModelMethod method, BeanDefinitionRegistry registry) { + RootBeanDefinition beanDef = new JavaConfigBeanDefinition(); + + ConfigurationClass configClass = method.getDeclaringClass(); + + beanDef.setFactoryBeanName(configClass.getBeanName()); + beanDef.setFactoryMethodName(method.getName()); + + Bean bean = method.getRequiredAnnotation(Bean.class); + + Configuration defaults = configClass.getMetadata(); + + // consider scoping + beanDef.setScope(bean.scope()); + + // consider autowiring + if (bean.autowire() != AnnotationUtils.getDefaultValue(Bean.class, "autowire")) + beanDef.setAutowireMode(bean.autowire().value()); + else if (defaults.defaultAutowire() != AnnotationUtils.getDefaultValue(Configuration.class, "defaultAutowire")) + beanDef.setAutowireMode(defaults.defaultAutowire().value()); + + String beanName = method.getName(); + + // has this already been overriden (i.e.: via XML)? + if (containsBeanDefinitionIncludingAncestry(beanName, registry)) { + BeanDefinition existingBeanDef = getBeanDefinitionIncludingAncestry(beanName, registry); + + // is the existing bean definition one that was created by JavaConfig? + if (!(existingBeanDef instanceof JavaConfigBeanDefinition)) { + // no -> then it's an external override, probably XML + + // ensure that overriding is ok + if (bean.allowOverriding() == false) { + UsageError error = configClass.new IllegalBeanOverrideError(null, method); + throw new MalformedJavaConfigurationException(error); + } + + // overriding is legal, return immediately + logger.info(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)); + return; + } + } + + // propagate this bean's 'role' attribute + beanDef.setRole(bean.role()); + + // consider aliases + for (String alias : bean.aliases()) + registry.registerAlias(beanName, alias); + + // TODO: re-enable for Lazy support +// // is this bean marked as primary for disambiguation? +// if (bean.primary() == Primary.TRUE) +// beanDef.setPrimary(true); +// +// // is this bean lazily instantiated? +// if ((bean.lazy() == Lazy.TRUE) +// || ((bean.lazy() == Lazy.UNSPECIFIED) && (defaults.defaultLazy() == Lazy.TRUE))) +// beanDef.setLazyInit(true); + + // does this bean have a custom init-method specified? + String initMethodName = bean.initMethodName(); + if (hasText(initMethodName)) + beanDef.setInitMethodName(initMethodName); + + // does this bean have a custom destroy-method specified? + String destroyMethodName = bean.destroyMethodName(); + if (hasText(destroyMethodName)) + beanDef.setDestroyMethodName(destroyMethodName); + + // TODO: re-enable for @ScopedProxy support + // is this method annotated with @ScopedProxy? +// ScopedProxy scopedProxy = method.getAnnotation(ScopedProxy.class); +// if (scopedProxy != null) { +// RootBeanDefinition targetDef = beanDef; +// +// // Create a scoped proxy definition for the original bean name, +// // "hiding" the target bean in an internal target definition. +// String targetBeanName = ScopedProxy.Util.resolveHiddenScopedProxyBeanName(beanName); +// RootBeanDefinition scopedProxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); +// scopedProxyDefinition.getPropertyValues().addPropertyValue("targetBeanName", targetBeanName); +// +// if (scopedProxy.proxyTargetClass()) +// targetDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); +// // ScopedFactoryBean's "proxyTargetClass" default is TRUE, so we +// // don't need to set it explicitly here. +// else +// scopedProxyDefinition.getPropertyValues().addPropertyValue("proxyTargetClass", Boolean.FALSE); +// +// // The target bean should be ignored in favor of the scoped proxy. +// targetDef.setAutowireCandidate(false); +// +// // Register the target bean as separate bean in the factory +// registry.registerBeanDefinition(targetBeanName, targetDef); +// +// // replace the original bean definition with the target one +// beanDef = scopedProxyDefinition; +// } + + // TODO: re-enable for @Meta support + // does this bean method have any @Meta annotations? +// for (Meta meta : bean.meta()) +// beanDef.addMetadataAttribute(new BeanMetadataAttribute(meta.key(), meta.value())); + + if(bean.dependsOn().length > 0) + beanDef.setDependsOn(bean.dependsOn()); + + logger.info(format("Registering bean definition for @Bean method %s.%s()", + configClass.getName(), beanName)); + + registry.registerBeanDefinition(beanName, beanDef); + + } + + private boolean containsBeanDefinitionIncludingAncestry(String beanName, BeanDefinitionRegistry registry) { + try { + getBeanDefinitionIncludingAncestry(beanName, registry); + return true; + } catch (NoSuchBeanDefinitionException ex) { + return false; + } + } + + private BeanDefinition getBeanDefinitionIncludingAncestry(String beanName, BeanDefinitionRegistry registry) { + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, registry); + ConfigurableListableBeanFactory clbf = (ConfigurableListableBeanFactory)registry; + + do { + if (clbf.containsBeanDefinition(beanName)) + return registry.getBeanDefinition(beanName); + + BeanFactory parent = clbf.getParentBeanFactory(); + if (parent == null) { + clbf = null; + } else if (parent instanceof ConfigurableListableBeanFactory) { + clbf = (ConfigurableListableBeanFactory) parent; + // TODO: re-enable +// } else if (parent instanceof AbstractApplicationContext) { +// clbf = ((AbstractApplicationContext) parent).getBeanFactory(); + } else { + throw new IllegalStateException("unknown parent type: " + parent.getClass().getName()); + } + } while (clbf != null); + + throw new NoSuchBeanDefinitionException( + format("No bean definition matching name '%s' " + + "could be found in %s or its ancestry", beanName, registry)); + } + +} + +/** + * {@link RootBeanDefinition} marker subclass used to signify that a bean definition + * created by JavaConfig as opposed to any other configuration source. Used in bean + * overriding cases where it's necessary to determine whether the bean definition was created + * externally (e.g. via XML). + */ +@SuppressWarnings("serial") +// TODO: SJC-242 what to do about JavaConfigBeanDefinition? +class JavaConfigBeanDefinition extends RootBeanDefinition { +} \ No newline at end of file diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Configuration.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Configuration.java new file mode 100644 index 0000000000..b8e33be842 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Configuration.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.beans.factory.annotation.Required; +import org.springframework.stereotype.Component; + + +/** + * Annotation indicating that a class is a "Java Configuration" class, meaning that it exposes one + * or more {@link Bean} methods. Holds similar information to that held in the default values of a + * bean factory; can generally be thought of as the JavaConfig equivalent of XML's 'beans' root + * element. + * + *

Note however that the information here is not used to populate the defaults of the owning bean + * factory, which would affect other configurations. In the style of the Java configuration + * mechanism generally, each Java configuration class is kept isolated.

+ * + *

Configuration-annotated classes are also candidates for component scanning thanks to the fact + * that this annotation is meta-annotated with {@link Component @Component}.

+ * + * @author Rod Johnson + * @author Chris Beams + */ +@Component +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Configuration { + + /** + * Configuration name. Allow different variants, such as test, production + * etc. Default will always match. + * @return + */ + String[] names() default ""; + + /** + * Specifies the default autowiring strategy. + * + * @see Autowire + * @return + */ + Autowire defaultAutowire() default Autowire.INHERITED; + +// /** +// * Dependency check strategy. By default, the dependency check is +// * unspecified, that is the default Spring option will apply. In most cases, +// * it means no dependency check will be done. +// * +// * @see DependencyCheck +// * @return +// */ +// DependencyCheck defaultDependencyCheck() default DependencyCheck.UNSPECIFIED; +// +// /** +// * Bean instantiation strategy. By default, it is unspecified. +// * +// * @see Lazy +// * @return +// */ +// Lazy defaultLazy() default Lazy.UNSPECIFIED; + + /** + * Do we autowire with aspects from the enclosing factory scope? + */ + boolean useFactoryAspects() default false; + + /** + * Do we check {@link Required @Required} methods to make sure they've been + * called? + */ + boolean checkRequired() default false; + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Factory.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Factory.java new file mode 100644 index 0000000000..dfe1fbcf70 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Factory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.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 net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.NoOp; + +/** + * Meta-annotation used to identify annotations as producers of beans and/or values. + * + * @author Chris Beams + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +@Documented +public @interface Factory { + + /** + * Specifies which registrar (if any) should be used to register + * bean definitions for this {@link Factory} method. + */ + Class registrarType(); + + /** + * Specifies what (if any) callback should be used when processing this {@link Factory} method. + * Defaults to CGLIB's {@link NoOp}, which does nothing. + * TODO: rename (interceptorType)? to keep with the -or|-ar nomenclature + */ + Class callbackType() default NoOp.class; + + /** + * TODO: document + * TODO: rename + */ + Class[] validatorTypes() default {}; +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Validator.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Validator.java new file mode 100644 index 0000000000..5bc29edd75 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/annotation/Validator.java @@ -0,0 +1,14 @@ +package org.springframework.config.java.annotation; + +import java.util.List; + +import org.springframework.config.java.model.UsageError; + +/** Marker interface */ +//TODO: SJC-242 document +//TODO: SJC-242 rename +public interface Validator { + boolean supports(Object object); + + void validate(Object object, List errors); +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/AddAnnotationAdapter.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/AddAnnotationAdapter.java new file mode 100644 index 0000000000..96ae3bfefe --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/AddAnnotationAdapter.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.enhancement; + +import net.sf.cglib.asm.Constants; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; + + + +/** + * Transforms a class by adding bytecode for a class-level annotation. + * Checks to ensure that the desired annotation is not already present + * before adding. + *

+ * This class was originally adapted from examples the ASM 3.0 documentation. + * + * @author Chris Beams + */ +class AddAnnotationAdapter extends ClassAdapter { + private String annotationDesc; + private boolean isAnnotationPresent; + + /** + * Creates a new AddAnnotationAdapter instance. + * + * @param cv the ClassVisitor delegate + * @param annotationDesc name of the annotation to be added + * (in type descriptor format) + */ + public AddAnnotationAdapter(ClassVisitor cv, String annotationDesc) { + super(cv); + this.annotationDesc = annotationDesc; + } + + /** + * Ensures that the version of the resulting class is Java 5 or better. + */ + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + int v = (version & 0xFF) < Constants.V1_5 ? Constants.V1_5 : version; + cv.visit(v, access, name, signature, superName, interfaces); + } + + /** + * Checks to ensure that the desired annotation is not already present. + */ + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (visible && desc.equals(annotationDesc)) { + isAnnotationPresent = true; + } + return cv.visitAnnotation(desc, visible); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + addAnnotation(); + cv.visitInnerClass(name, outerName, innerName, access); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + addAnnotation(); + return cv.visitField(access, name, desc, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + addAnnotation(); + return cv.visitMethod(access, name, desc, signature, exceptions); + } + + /** + * Kicks off the process of actually adding the desired annotation. + * + * @see #addAnnotation() + */ + @Override + public void visitEnd() { + addAnnotation(); + cv.visitEnd(); + } + + /** + * Actually adds the desired annotation. + */ + private void addAnnotation() { + if (!isAnnotationPresent) { + AnnotationVisitor av = cv.visitAnnotation(annotationDesc, true); + if (av != null) { + av.visitEnd(); + } + isAnnotationPresent = true; + } + } +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/CglibConfigurationEnhancer.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/CglibConfigurationEnhancer.java new file mode 100644 index 0000000000..064d930662 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/CglibConfigurationEnhancer.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.enhancement; + +import static java.lang.String.*; +import static org.springframework.config.java.Util.*; +import static org.springframework.util.Assert.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; + +import net.sf.cglib.core.DefaultGeneratorStrategy; +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.CallbackFilter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.NoOp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.config.java.annotation.BeanDefinitionRegistrar; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.config.java.model.ModelMethod; + + + +/** + * Enhances {@link Configuration} classes by generating a CGLIB subclass capable of + * interacting with the Spring container to respect bean semantics. + * + * @see #enhance(String) + * + * @author Chris Beams + */ +public class CglibConfigurationEnhancer implements ConfigurationEnhancer { + + private static final Log log = LogFactory.getLog(CglibConfigurationEnhancer.class); + + private final ArrayList> callbackTypes = + new ArrayList>(); + + private final LinkedHashSet handlers = + new LinkedHashSet(); + + private final ArrayList callbackInstances = + new ArrayList(); + + private final CallbackFilter callbackFilter = + new CallbackFilter() { + public int accept(Method candidateMethod) { + Iterator iter = handlers.iterator(); + for(int i=0; iter.hasNext(); i++) + if(iter.next().accepts(candidateMethod)) + return i; + + throw new IllegalStateException(format("No registered handler is capable of " + + "handling method [%s]. Perhaps you forgot to register a catch-all registrar?", + candidateMethod.getName())); + } + }; + + + /** + * Creates a new {@link CglibConfigurationEnhancer} instance. + */ + public CglibConfigurationEnhancer(DefaultListableBeanFactory beanFactory, ConfigurationModel model) { + notNull(beanFactory, "beanFactory must be non-null"); + notNull(model, "model must be non-null"); + + populateHandlersAndCallbacks(beanFactory, model); + } + + + /** + * Reads the contents of {@code model} in order to populate {@link #handlers}, + * {@link #callbackInstances} and {@link #callbackTypes} appropriately. + * + * @see #callbackFilter + */ + private void populateHandlersAndCallbacks(DefaultListableBeanFactory beanFactory, ConfigurationModel model) { + + for (ConfigurationClass configClass : model.getAllConfigurationClasses()) { + for (ModelMethod method : configClass.getMethods()) { + handlers.add(method.getRegistrar()); + + Callback callback = method.getCallback(); + + if(callback instanceof BeanFactoryAware) + ((BeanFactoryAware)callback).setBeanFactory(beanFactory); + + callbackInstances.add(callback); + } + } + + handlers.add(new InitializingBeanRegistrar()); + callbackInstances.add(new InitializingBeanCallback(beanFactory)); + + // register a 'catch-all' registrar + handlers.add(new BeanDefinitionRegistrar() { + + public boolean accepts(Method method) { + return true; + } + + public void register(ModelMethod method, BeanDefinitionRegistry registry) { + // no-op + } + }); + callbackInstances.add(NoOp.INSTANCE); + + for(Callback callback : callbackInstances) + callbackTypes.add(callback.getClass()); + } + + + /** + * Loads the specified class and generates a CGLIB subclass of it equipped with container-aware + * callbacks capable of respecting scoping and other bean semantics. + */ + public String enhance(String configClassName) { + if (log.isInfoEnabled()) + log.info("Enhancing " + configClassName); + + Class superclass = loadRequiredClass(configClassName); + + Class subclass = createClass(newEnhancer(superclass), superclass); + + subclass = nestOneClassDeeperIfAspect(superclass, subclass); + + if (log.isInfoEnabled()) + log.info(format("Successfully enhanced %s; enhanced class name is: %s", + configClassName, subclass.getName())); + + return subclass.getName(); + } + + /** + * Creates a new CGLIB {@link Enhancer} instance. + */ + private Enhancer newEnhancer(Class superclass) { + Enhancer enhancer = new Enhancer(); + + // because callbackFilter and callbackTypes are dynamically populated + // there's no opportunity for caching. This does not appear to be causing + // any performance problem. + enhancer.setUseCache(false); + + enhancer.setSuperclass(superclass); + enhancer.setInterfaces(new Class[]{InitializingBean.class}); + enhancer.setUseFactory(false); + enhancer.setCallbackFilter(callbackFilter); + enhancer.setCallbackTypes(callbackTypes.toArray(new Class[]{})); + + return enhancer; + } + + /** + * Uses enhancer to generate a subclass of superclass, ensuring that + * {@link #callbackInstances} are registered for the new subclass. + */ + private Class createClass(Enhancer enhancer, Class superclass) { + Class subclass = enhancer.createClass(); + + // see #registerThreadLocalCleanupBeanDefinition + Enhancer.registerCallbacks(subclass, callbackInstances.toArray(new Callback[] {})); + //Enhancer.registerStaticCallbacks(subclass, callbackInstances.toArray(new Callback[] {})); + + return subclass; + } + + /** + * Works around a constraint imposed by the AspectJ 5 annotation-style programming model. See + * comments inline for detail. + * + * @return original subclass instance unless superclass is annnotated with @Aspect, in which + * case a subclass of the subclass is returned + */ + private Class nestOneClassDeeperIfAspect(Class superclass, Class origSubclass) { + boolean superclassIsAnAspect = false; + + // check for @Aspect by name rather than by class literal to avoid + // requiring AspectJ as a runtime dependency. + for(Annotation anno : superclass.getAnnotations()) + if(anno.annotationType().getName().equals("org.aspectj.lang.annotation.Aspect")) + superclassIsAnAspect = true; + + if(!superclassIsAnAspect) + return origSubclass; + + // the superclass is annotated with AspectJ's @Aspect. + // this means that we must create a subclass of the subclass + // in order to avoid some guard logic in Spring core that disallows + // extending a concrete aspect class. + Enhancer enhancer = newEnhancer(origSubclass); + enhancer.setStrategy(new DefaultGeneratorStrategy() { + @Override + protected byte[] transform(byte[] b) throws Exception { + ClassWriter writer = new ClassWriter(false); + ClassAdapter adapter = + new AddAnnotationAdapter(writer, "Lorg/aspectj/lang/annotation/Aspect;"); + ClassReader reader = new ClassReader(b); + reader.accept(adapter, false); + return writer.toByteArray(); + } + }); + + // create a subclass of the original subclass + Class newSubclass = createClass(enhancer, origSubclass); + + return newSubclass; + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/ConfigurationEnhancer.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/ConfigurationEnhancer.java new file mode 100644 index 0000000000..10c2184d47 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/ConfigurationEnhancer.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.enhancement; + + +/** TODO: JAVADOC */ +public interface ConfigurationEnhancer { + + String enhance(String configClassName); + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanCallback.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanCallback.java new file mode 100644 index 0000000000..dc1d3fb793 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.enhancement; + +import java.lang.reflect.Method; + +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +// TODO: should not be necessary to make configurations implement InitializingBean anymore once @Value is in the picture +class InitializingBeanCallback implements MethodInterceptor { + + private final DefaultListableBeanFactory beanFactory; + + public InitializingBeanCallback(DefaultListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + + // TODO: SJC-242 ExternalValueInjector - revisit this line (inline method?) + // TODO: should be handled by @Value + //new ExternalValueInjector(beanFactory).injectExternalValues(obj); + + // only call the superclass afterPropertiesSet method if it is actually implemented + if(!InitializingBean.class.equals(method.getDeclaringClass())) + return proxy.invokeSuper(obj, args); + + return Void.TYPE; + } +} \ No newline at end of file diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanRegistrar.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanRegistrar.java new file mode 100644 index 0000000000..fe7f2a000f --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/enhancement/InitializingBeanRegistrar.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.enhancement; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.config.java.annotation.BeanDefinitionRegistrar; +import org.springframework.config.java.model.ModelMethod; + +class InitializingBeanRegistrar implements BeanDefinitionRegistrar { + public boolean accepts(Method method) { + return method.getName().equals("afterPropertiesSet") + && method.getReturnType().equals(void.class); + } + + public void register(ModelMethod method, BeanDefinitionRegistry registry) { + // no-op + } +} \ No newline at end of file diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/AsmJavaConfigBeanDefinitionReader.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/AsmJavaConfigBeanDefinitionReader.java new file mode 100644 index 0000000000..2fb2e8d3ac --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/AsmJavaConfigBeanDefinitionReader.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.factory.support; + +import static org.springframework.config.java.Util.*; + +import java.util.ArrayList; +import java.util.Map; + +import org.objectweb.asm.ClassReader; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.internal.parsing.ConfigurationParser; +import org.springframework.config.java.internal.parsing.asm.AsmConfigurationParser; +import org.springframework.config.java.internal.parsing.asm.AsmUtils; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.config.java.model.MalformedJavaConfigurationException; +import org.springframework.config.java.model.UsageError; +import org.springframework.core.io.ClassPathResource; + + +/** + * Uses ASM to parse {@link Configuration @Configuration} classes. Fashioned after the + * {@link BeanDefinitionReader} hierarchy, but does not extend or implement any of its + * types because differences were significant enough to merit the departure. + * + * @see AsmConfigurationParser + * + * @author Chris Beams + */ +public class AsmJavaConfigBeanDefinitionReader { + + private final ConfigurationModelBeanDefinitionReader modelBeanDefinitionReader; + + /** + * Creates a new {@link AsmJavaConfigBeanDefinitionReader}. + * + * @param registry {@link BeanDefinitionRegistry} into which new bean definitions will be + * registered as they are read from Configuration classes. + */ + public AsmJavaConfigBeanDefinitionReader(DefaultListableBeanFactory beanFactory) { + this.modelBeanDefinitionReader = new ConfigurationModelBeanDefinitionReader(beanFactory); + } + + /** + * Parses each {@link Configuration} class specified by configClassResources and registers + * individual bean definitions from those Configuration classes into the BeanDefinitionRegistry + * supplied during construction. + */ + public int loadBeanDefinitions(ConfigurationModel model, Map configClassResources) throws BeanDefinitionStoreException { + ConfigurationParser parser = new AsmConfigurationParser(model); + + for (String id : configClassResources.keySet()) { + String resourcePath = configClassResources.get(id).getPath(); + ClassReader configClassReader = AsmUtils.newClassReader(getClassAsStream(resourcePath)); + parser.parse(configClassReader, id); + } + + ArrayList errors = new ArrayList(); + model.validate(errors); + if (errors.size() > 0) + throw new MalformedJavaConfigurationException(errors.toArray(new UsageError[] { })); + + return modelBeanDefinitionReader.loadBeanDefinitions(model); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/ConfigurationModelBeanDefinitionReader.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/ConfigurationModelBeanDefinitionReader.java new file mode 100644 index 0000000000..dd181113cf --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/factory/support/ConfigurationModelBeanDefinitionReader.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.factory.support; + + +import static java.lang.String.*; +import static org.springframework.config.java.Util.*; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Comparator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionReader; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.annotation.Factory; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.config.java.model.ModelMethod; +import org.springframework.config.java.plugin.Extension; +import org.springframework.config.java.plugin.ExtensionAnnotationBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + + + +/** + * Reads a given fully-populated {@link ConfigurationModel}, 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 {@link ConfigurationModel} is not a + * {@link Resource}. + * + * @author Chris Beams + */ +// TODO: Unit test +public class ConfigurationModelBeanDefinitionReader { + + private static final Log log = LogFactory.getLog(ConfigurationModelBeanDefinitionReader.class); + + private final DefaultListableBeanFactory beanFactory; + + + /** + * Creates a new {@link ConfigurationModelBeanDefinitionReader} instance. + */ + public ConfigurationModelBeanDefinitionReader(DefaultListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + /** + * Reads {@code model}, registering bean definitions with {@link #beanFactory} + * based on its contents. + * + * @return number of bean definitions generated + */ + public int loadBeanDefinitions(ConfigurationModel model) { + int initialBeanDefCount = beanFactory.getBeanDefinitionCount(); + + for (ConfigurationClass configClass : model.getAllConfigurationClasses()) + loadBeanDefinitionsForConfigurationClass(configClass); + + return beanFactory.getBeanDefinitionCount() - initialBeanDefCount; + } + + /** + * Reads a particular {@link ConfigurationClass}, registering bean definitions + * for the class itself, all its {@link Factory} methods and all its {@link Extension} + * annotations. + */ + private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) { + doLoadBeanDefinitionForConfigurationClass(configClass); + + for (ModelMethod method : configClass.getMethods()) + loadBeanDefinitionsForModelMethod(method); + + Annotation[] pluginAnnotations = configClass.getPluginAnnotations(); + Arrays.sort(pluginAnnotations, new PluginComparator()); + for (Annotation annotation : pluginAnnotations) + loadBeanDefinitionsForExtensionAnnotation(annotation); + } + + /** + * Registers the {@link Configuration} class itself as a bean definition. + */ + private void doLoadBeanDefinitionForConfigurationClass(ConfigurationClass configClass) { + Configuration metadata = configClass.getMetadata(); + + if (metadata.checkRequired() == true) { + RootBeanDefinition requiredAnnotationPostProcessor = new RootBeanDefinition(); + Class beanClass = RequiredAnnotationBeanPostProcessor.class; + String beanName = beanClass.getName() + "#0"; + requiredAnnotationPostProcessor.setBeanClass(beanClass); + requiredAnnotationPostProcessor.setResourceDescription("ensures @Required methods have been invoked"); + beanFactory.registerBeanDefinition(beanName, requiredAnnotationPostProcessor); + } + + GenericBeanDefinition configBeanDef = new GenericBeanDefinition(); + configBeanDef.setBeanClassName(configClass.getName()); + + String configBeanName = configClass.getBeanName(); + + // consider the case where it's already been defined (probably in XML) + // and potentially has PropertyValues and ConstructorArgs) + if (beanFactory.containsBeanDefinition(configBeanName)) { + if (log.isInfoEnabled()) + log.info(format("Copying property and constructor arg values from existing bean definition for " + + "@Configuration class %s to new bean definition", configBeanName)); + AbstractBeanDefinition existing = (AbstractBeanDefinition)beanFactory.getBeanDefinition(configBeanName); + configBeanDef.setPropertyValues(existing.getPropertyValues()); + configBeanDef.setConstructorArgumentValues(existing.getConstructorArgumentValues()); + configBeanDef.setResource(existing.getResource()); + } + + if (log.isInfoEnabled()) + log.info(format("Registering bean definition for @Configuration class %s", configBeanName)); + + beanFactory.registerBeanDefinition(configBeanName, configBeanDef); + } + + + /** + * Reads a particular {@link ModelMethod}, registering bean definitions + * with {@link #beanFactory} based on its contents. + * + * @see Factory + */ + private void loadBeanDefinitionsForModelMethod(ModelMethod method) { + method.getRegistrar().register(method, beanFactory); + } + + @SuppressWarnings("unchecked") + private void loadBeanDefinitionsForExtensionAnnotation(Annotation anno) { + //ExtensionAnnotationUtils.getRegistrarFor(anno).registerBeanDefinitionsWith(beanFactory); + // there is a fixed assumption that in order for this annotation to have + // been registered in the first place, it must be meta-annotated with @Plugin + // assert this as an invariant now + Class annoClass = anno.getClass(); + Extension extensionAnno = AnnotationUtils.findAnnotation(annoClass, Extension.class); + Assert.isTrue(extensionAnno != null, + format("%s annotation is not annotated as a @%s", + annoClass, Extension.class.getSimpleName())); + + Class extHandlerClass = extensionAnno.handler(); + + ExtensionAnnotationBeanDefinitionRegistrar extHandler = getInstance(extHandlerClass); + extHandler.handle(anno, beanFactory); + } + + private static class PluginComparator implements Comparator { + public int compare(Annotation a1, Annotation a2) { + Integer i1 = getOrder(a1); + Integer i2 = getOrder(a2); + return i1.compareTo(i2); + } + + private Integer getOrder(Annotation a) { + Extension plugin = a.annotationType().getAnnotation(Extension.class); + if(plugin == null) + throw new IllegalArgumentException( + "annotation was not annotated with @Plugin: " + a.annotationType()); + return plugin.order(); + } + } + + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/ConfigurationParser.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/ConfigurationParser.java new file mode 100644 index 0000000000..fe7523325e --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/ConfigurationParser.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing; + +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.model.ConfigurationModel; + + +/** + * Parses a {@link Configuration} class definition, usually into a {@link ConfigurationModel}. + *

+ * This interface aids in separating the process of reading a class file (via reflection, ASM, etc) + * from the process of registering bean definitions based on the content of that class. + * + * @see org.springframework.config.java.internal.parsing.asm.AsmConfigurationParser + * @see org.springframework.config.java.model.ConfigurationModel + * @see org.springframework.config.java.internal.factory.support.ConfigurationModelBeanDefinitionReader + * + * @author Chris Beams + */ +public interface ConfigurationParser { + + /** + * Parse the Configuration object represented by configurationSource. + * + * @param configurationSource representation of a Configuration class, may be java.lang.Class, + * ASM representation or otherwise + * + * @see org.springframework.config.java.annotation.Configuration + */ + void parse(Object configurationSource); + + /** + * Optionally propagate a custom name for this configurationSource. Usually this id + * corresponds to the name of a Configuration bean as declared in a beans XML. + * + * @param configurationSource representation of a Configuration class, may be java.lang.Class, + * ASM representation or otherwise + * @param configurationId name of this configuration class, probably corresponding to a + * bean id + * + * @see org.springframework.config.java.annotation.Configuration + * @see org.springframework.config.java.process.ConfigurationPostProcessor + */ + void parse(Object configurationSource, String configurationId); + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AnnotationAdapter.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AnnotationAdapter.java new file mode 100644 index 0000000000..94ec8d30b2 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AnnotationAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import org.objectweb.asm.AnnotationVisitor; + + +/** + * An empty AnnotationVisitor that delegates to another AnnotationVisitor. + * This class can be used as a super class to quickly implement + * useful annotation adapter classes, just by overriding the necessary + * methods. Note that for some reason, ASM doesn't provide this class + * (it does provide MethodAdapter and ClassAdapter), thus we're following + * the general pattern and adding our own here. + * + * @author Chris Beams + */ +class AnnotationAdapter implements AnnotationVisitor { + + private AnnotationVisitor delegate; + + /** + * Creates a new AnnotationAdapter instance that will delegate + * all its calls to delegate. + * + * @param delegate In most cases, the delegate will simply be + * {@link AsmUtils#EMPTY_VISITOR} + */ + public AnnotationAdapter(AnnotationVisitor delegate) { + this.delegate = delegate; + } + + public void visit(String arg0, Object arg1) { + delegate.visit(arg0, arg1); + } + + public AnnotationVisitor visitAnnotation(String arg0, String arg1) { + return delegate.visitAnnotation(arg0, arg1); + } + + public AnnotationVisitor visitArray(String arg0) { + return delegate.visitArray(arg0); + } + + public void visitEnum(String arg0, String arg1, String arg2) { + delegate.visitEnum(arg0, arg1, arg2); + } + + public void visitEnd() { + delegate.visitEnd(); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmConfigurationParser.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmConfigurationParser.java new file mode 100644 index 0000000000..f6f76cad1e --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmConfigurationParser.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + + + +import org.objectweb.asm.ClassReader; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.internal.parsing.ConfigurationParser; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.util.Assert; + + +/** + * ASM-based implementation of {@link ConfigurationParser}. Avoids reflection and eager classloading + * in order to interoperate effectively with tooling (Spring IDE). + * + * @see org.springframework.config.java.model.AsmConfigurationParserTests + + * @author Chris Beams + */ +public class AsmConfigurationParser implements ConfigurationParser { + + /** + * Model to be populated during calls to {@link #parse(Object)} + */ + private final ConfigurationModel model; + + /** + * Creates a new parser instance that will be used to populate model. + * + * @param model model to be populated by each successive call to {@link #parse(Object)} + */ + public AsmConfigurationParser(ConfigurationModel model) { + this.model = model; + } + + /** + * Convenience implementation, delegates to {@link #parse(Object, String)}, + * passing in {@code null} for the configurationId. + * + * @param configurationSource must be an ASM {@link ClassReader} + */ + public void parse(Object configurationSource) { + parse(configurationSource, null); + } + + /** + * Parse the {@link Configuration @Configuration} class encapsulated by + * configurationSource. + * + * @param configurationSource must be an ASM {@link ClassReader} + * @param configurationId may be null, but if populated represents the bean id + * (assumes that this configuration class was configured via XML) + */ + public void parse(Object configurationSource, String configurationId) { + Assert.isInstanceOf(ClassReader.class, configurationSource, + "configurationSource must be an ASM ClassReader"); + + ConfigurationClass configClass = new ConfigurationClass(); + configClass.setBeanName(configurationId); + + parse((ClassReader) configurationSource, configClass); + } + + /** + * Kicks off visiting configClass with {@link ConfigurationClassVisitor} + */ + private void parse(ClassReader reader, ConfigurationClass configClass) { + reader.accept(new ConfigurationClassVisitor(configClass, model), false); + model.add(configClass); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmUtils.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmUtils.java new file mode 100644 index 0000000000..af57d7dc44 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/AsmUtils.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.commons.EmptyVisitor; +import org.springframework.config.java.Util; + + +/** + * Various utility methods commonly used when interacting with ASM. + */ +public class AsmUtils { + + public static final EmptyVisitor EMPTY_VISITOR = new EmptyVisitor(); + + private static final Log log = LogFactory.getLog(AsmUtils.class); + + /** + * @param className a standard, dot-delimeted, fully-qualified Java class name + * @return internal version of className, as per ASM guide section 2.1.2 "Internal Names" + */ + public static String convertClassNameToInternalName(String className) { + return className.replace('.', '/'); + } + + /** + * Convert a type descriptor to a classname suitable for classloading + * with Class.forName(). + * + * @param typeDescriptor see ASM guide section 2.1.3 + */ + public static String convertTypeDescriptorToClassName(String typeDescriptor) { + final String internalName; // See ASM guide section 2.1.2 + + // TODO: SJC-242 should catch all possible cases. use case statement and switch on char + // TODO: SJC-242 converting from primitive to object here won't be intuitive to users + 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 + */ + public static String getReturnTypeFromMethodDescriptor(String methodDescriptor) { + String returnTypeDescriptor = methodDescriptor.substring(methodDescriptor.indexOf(')')+1); + return convertTypeDescriptorToClassName(returnTypeDescriptor); + } + + /** + * Creates a new ASM {@link ClassReader} for pathToClass. Appends '.class' + * to pathToClass before attempting to load. + * + * @throws RuntimeException if pathToClass+.class cannot be found on the classpath + * @throws RuntimeException if an IOException occurs when creating the new ClassReader + */ + public static ClassReader newClassReader(String pathToClass) { + InputStream is = Util.getClassAsStream(pathToClass); + return newClassReader(is); + } + + /** + * Convenience method that simply returns a new ASM {@link ClassReader} instance based on + * the supplied bytes byte array. This method is exactly equivalent to calling + * new ClassReader(byte[]), and is mainly provided for symmetry with usage of + * {@link #newClassReader(InputStream)}. + * + * @param bytes byte array that will be provided as input to the new ClassReader instance. + * + * @return + */ + public static ClassReader newClassReader(byte[] bytes) { + return new ClassReader(bytes); + } + + /** + * Convenience method that creates and returns a new ASM {@link ClassReader} for the given + * InputStream is, closing the InputStream after creating the ClassReader and rethrowing + * any IOException thrown during ClassReader instantiation as an unchecked exception. Logs and ignores + * any IOException thrown when closing the InputStream. + * + * @param is InputStream that will be provided to the new ClassReader instance. + */ + public static ClassReader newClassReader(InputStream is) { + try { + return new ClassReader(is); + } catch (IOException ex) { + throw new RuntimeException("An unexpected exception occurred while creating ASM ClassReader: " + ex); + } finally { + try { + is.close(); + } catch (IOException ex) { + log.error("Ignoring exception thrown while closing InputStream", ex); + } + } + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassMethodVisitor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassMethodVisitor.java new file mode 100644 index 0000000000..1393661ce2 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassMethodVisitor.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import static org.springframework.config.java.Util.*; +import static org.springframework.config.java.internal.parsing.asm.AsmUtils.*; +import static org.springframework.config.java.internal.parsing.asm.MutableAnnotationUtils.*; +import static org.springframework.util.ClassUtils.*; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodAdapter; +import org.objectweb.asm.Opcodes; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.annotation.Factory; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.ModelClass; +import org.springframework.config.java.model.ModelMethod; + +/** + * Visits a single method declared in a given {@link Configuration} class. Determines whether the + * method is a {@link Factory} method and if so, adds it to the {@link ConfigurationClass}. + * + * @author Chris Beams + */ +class ConfigurationClassMethodVisitor extends MethodAdapter { + + private final ConfigurationClass configClass; + private final String methodName; + private final int modifiers; + private final ModelClass returnType; + private final ArrayList annotations = new ArrayList(); + + private boolean isModelMethod = false; + private int lineNumber; + + /** + * Creates a new {@link ConfigurationClassMethodVisitor} instance. + * + * @param configClass model object to which this method will be added + * @param methodName name of the method declared in the {@link Configuration} class + * @param methodDescriptor ASM representation of the method signature + * @param modifiers modifiers for this method + */ + public ConfigurationClassMethodVisitor(ConfigurationClass configClass, String methodName, + String methodDescriptor, int modifiers) { + super(AsmUtils.EMPTY_VISITOR); + + this.configClass = configClass; + this.methodName = methodName; + this.returnType = initReturnTypeFromMethodDescriptor(methodDescriptor); + this.modifiers = modifiers; + } + + /** + * Visits a single annotation on this method. Will be called once for each + * annotation present (regardless of its RetentionPolicy). + */ + @Override + public AnnotationVisitor visitAnnotation(String annoTypeDesc, boolean visible) { + String annoClassName = AsmUtils.convertTypeDescriptorToClassName(annoTypeDesc); + + Class annoClass = loadToolingSafeClass(annoClassName); + + if(annoClass == null) + return super.visitAnnotation(annoTypeDesc, visible); + + Annotation annotation = createMutableAnnotation(annoClass); + + annotations.add(annotation); + + return new MutableAnnotationVisitor(annotation); + } + + /** + * Provides the line number of this method within its declaring class. In reality, + * this number is always inaccurate - lineNo represents the line number + * of the first instruction in this method. Method declaration line numbers are + * not in any way tracked in the bytecode. Any tooling or output that reads this + * value will have to compensate and estimate where the actual method declaration + * is. + */ + @Override + public void visitLineNumber(int lineNo, Label start) { + this.lineNumber = lineNo; + } + + /** + * Parses through all {@link #annotations} on this method in order to determine whether + * it is a {@link Factory} method or not and if so adds it to the + * enclosing {@link #configClass}. + */ + @Override + public void visitEnd() { + for(Annotation anno : annotations) { + if(anno.annotationType().getAnnotation(Factory.class) != null) { + isModelMethod = true; + break; + } + } + + if(!isModelMethod) + return; + + Annotation[] annoArray = annotations.toArray(new Annotation[] { }); + ModelMethod method = new ModelMethod(methodName, modifiers, returnType, annoArray); + method.setLineNumber(lineNumber); + configClass.addMethod(method); + } + + /** + * Determines return type from ASM methodDescriptor and determines whether + * that type is an interface. + */ + private static ModelClass initReturnTypeFromMethodDescriptor(String methodDescriptor) { + final ModelClass returnType = new ModelClass(getReturnTypeFromMethodDescriptor(methodDescriptor)); + + // detect whether the return type is an interface + newClassReader(convertClassNameToResourcePath(returnType.getName())).accept( + new ClassAdapter(AsmUtils.EMPTY_VISITOR) { + @Override + public void visit(int arg0, int arg1, String arg2, String arg3, String arg4, String[] arg5) { + returnType.setInterface((arg1 & Opcodes.ACC_INTERFACE) == Opcodes.ACC_INTERFACE); + } + }, false); + + return returnType; + } +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassVisitor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassVisitor.java new file mode 100644 index 0000000000..a5a5d63ffe --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/ConfigurationClassVisitor.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import static org.springframework.config.java.Util.*; +import static org.springframework.config.java.internal.parsing.asm.MutableAnnotationUtils.*; +import static org.springframework.util.ClassUtils.*; + +import java.lang.annotation.Annotation; +import java.util.HashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.model.ConfigurationClass; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.config.java.plugin.Extension; +import org.springframework.util.ClassUtils; + + +/** + * Visits a {@link Configuration} class, populating a {@link ConfigurationClass} instance with + * information gleaned along the way. + * + * @author Chris Beams + */ +class ConfigurationClassVisitor extends ClassAdapter { + + private static final Log log = LogFactory.getLog(ConfigurationClassVisitor.class); + private static final String OBJECT_DESC = convertClassNameToResourcePath(Object.class.getName()); + + private final ConfigurationClass configClass; + private final ConfigurationModel model; + private final HashMap innerClasses = new HashMap(); + + private boolean processInnerClasses = true; + + public ConfigurationClassVisitor(ConfigurationClass configClass, ConfigurationModel model) { + super(AsmUtils.EMPTY_VISITOR); + this.configClass = configClass; + this.model = model; + } + + public void setProcessInnerClasses(boolean processInnerClasses) { + this.processInnerClasses = processInnerClasses; + } + + @Override + public void visitSource(String sourceFile, String debug) { + String resourcePath = + convertClassNameToResourcePath(configClass.getName()) + .substring(0, configClass.getName().lastIndexOf('.')+1) + .concat(sourceFile); + + configClass.setSource(resourcePath); + } + + @Override + public void visit(int classVersion, int modifiers, String classTypeDesc, String arg3, + String superTypeDesc, String[] arg5) { + visitSuperType(superTypeDesc); + + configClass.setName(convertResourcePathToClassName(classTypeDesc)); + + // ASM always adds ACC_SUPER to the opcodes/modifiers for class definitions. + // Unknown as to why (JavaDoc is silent on the matter), but it should be + // eliminated in order to comply with java.lang.reflect.Modifier values. + configClass.setModifiers(modifiers - Opcodes.ACC_SUPER); + } + + private void visitSuperType(String superTypeDesc) { + // traverse up the type hierarchy unless the next ancestor is java.lang.Object + if(OBJECT_DESC.equals(superTypeDesc)) + return; + + ConfigurationClassVisitor visitor = new ConfigurationClassVisitor(configClass, model); + + ClassReader reader = AsmUtils.newClassReader(superTypeDesc); + reader.accept(visitor, false); + } + + /** + * Visits a class level annotation on a {@link Configuration @Configuration} class. + * Accounts for all possible class-level annotations that are respected by JavaConfig + * including AspectJ's {@code @Aspect} annotation. + *

+ * Upon encountering such an annotation, update the {@link #configClass} model object + * appropriately, and then return an {@link AnnotationVisitor} implementation that can + * populate the annotation appropriately with data. + * + * @see MutableAnnotation + */ + @Override + public AnnotationVisitor visitAnnotation(String annoTypeDesc, boolean visible) { + String annoTypeName = AsmUtils.convertTypeDescriptorToClassName(annoTypeDesc); + + if (Configuration.class.getName().equals(annoTypeName)) { + Configuration mutableConfiguration = createMutableAnnotation(Configuration.class); + configClass.setMetadata(mutableConfiguration); + return new MutableAnnotationVisitor(mutableConfiguration); + } + + // TODO: re-enable for @Import support +// if (Import.class.getName().equals(annoTypeName)) { +// ImportStack importStack = ImportStackHolder.getImportStack(); +// +// if(importStack.contains(configClass)) +// throw new CircularImportException(configClass, importStack); +// +// importStack.push(configClass); +// +// return new ImportAnnotationVisitor(model); +// } + + // ------------------------------------- + // Detect @Plugin annotations + // ------------------------------------- + PluginAnnotationDetectingClassVisitor classVisitor = new PluginAnnotationDetectingClassVisitor(); + + String className = AsmUtils.convertTypeDescriptorToClassName(annoTypeDesc); + String resourcePath = ClassUtils.convertClassNameToResourcePath(className); + ClassReader reader = AsmUtils.newClassReader(resourcePath); + reader.accept(classVisitor, false); + + if(!classVisitor.hasPluginAnnotation()) + return super.visitAnnotation(annoTypeDesc, visible); + + Class annoType = loadToolingSafeClass(annoTypeName); + + if(annoType == null) + return super.visitAnnotation(annoTypeDesc, visible); + + Annotation pluginAnno = createMutableAnnotation(annoType); + configClass.addPluginAnnotation(pluginAnno); + return new MutableAnnotationVisitor(pluginAnno); + } + + private static class PluginAnnotationDetectingClassVisitor extends ClassAdapter { + private boolean hasPluginAnnotation = false; + private final Extension pluginAnnotation = createMutableAnnotation(Extension.class); + + public PluginAnnotationDetectingClassVisitor() { + super(AsmUtils.EMPTY_VISITOR); + } + + @Override + public AnnotationVisitor visitAnnotation(String typeDesc, boolean arg1) { + if(Extension.class.getName().equals(AsmUtils.convertTypeDescriptorToClassName(typeDesc))) { + hasPluginAnnotation = true; + return new MutableAnnotationVisitor(pluginAnnotation); + } + return super.visitAnnotation(typeDesc, arg1); + } + + public boolean hasPluginAnnotation() { + return hasPluginAnnotation; + } + + public Extension getPluginAnnotation() { + return pluginAnnotation; + } + } + + /** + * Delegates all {@link Configuration @Configuration} class method parsing to + * {@link ConfigurationClassMethodVisitor}. + */ + @Override + public MethodVisitor visitMethod(int modifiers, String methodName, String methodDescriptor, + String arg3, String[] arg4) { + + return new ConfigurationClassMethodVisitor(configClass, methodName, methodDescriptor, modifiers); + } + + /** + * Implementation deals with inner classes here even though it would have + * been more intuitive to deal with outer classes. Due to limitations in ASM + * (resulting from limitations in the VM spec) we cannot directly look for outer classes + * in all cases, so instead build up a model of {@link #innerClasses} and process + * declaring class logic in a kind of inverted manner. + */ + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + if(processInnerClasses == false) + return; + + String innerClassName = convertResourcePathToClassName(name); + String configClassName = configClass.getName(); + + // if the innerClassName is equal to configClassName, we just + // ran into the outermost inner class look up the outer class + // associated with this + if(innerClassName.equals(configClassName)) { + if(innerClasses.containsKey(outerName)) { + configClass.setDeclaringClass(innerClasses.get(outerName)); + } + return; + } + + ConfigurationClass innerConfigClass = new ConfigurationClass(); + + ConfigurationClassVisitor ccVisitor = + new ConfigurationClassVisitor(innerConfigClass, new ConfigurationModel()); + ccVisitor.setProcessInnerClasses(false); + + ClassReader reader = AsmUtils.newClassReader(name); + reader.accept(ccVisitor, false); + + if(innerClasses.containsKey(outerName)) + innerConfigClass.setDeclaringClass(innerClasses.get(outerName)); + + // is the inner class a @Configuration class? If so, add it to the list + if(innerConfigClass.getMetadata() != null) + innerClasses.put(name, innerConfigClass); + } +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotation.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotation.java new file mode 100644 index 0000000000..2c967aca96 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotation.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + + +/** + * Note: the visibility of this interface would be reduced to package-private + * save for an obscure restriction of JDK dynamic proxies. + * {@link MutableAnnotationUtils#createMutableAnnotation(Class)} creates a proxy based on two + * interfaces: this one, and whatever annotation is currently being parsed. + * The restriction is that both interfaces may not be package-private if they + * are in separate packages. In order to avoid unnecessarily restricting the + * visibility options for user-defined annotations, this interface becomes public. + * Because it is in the internal.* package, it won't pollute the public API, but + * developers should take caution not to use this annotation outside the + * internal.parsing.asm package. + * + * @author Chris Beams + */ +public interface MutableAnnotation { + void setAttributeValue(String attribName, Object attribValue); + Class getAttributeType(String attributeName); +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationArrayVisitor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationArrayVisitor.java new file mode 100644 index 0000000000..c82acb21b1 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationArrayVisitor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import static org.springframework.config.java.Util.*; +import static org.springframework.config.java.internal.parsing.asm.MutableAnnotationUtils.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.ArrayList; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.objectweb.asm.AnnotationVisitor; + + +/** TODO: JAVADOC */ +class MutableAnnotationArrayVisitor extends AnnotationAdapter { + + private static final Log log = LogFactory.getLog(MutableAnnotationArrayVisitor.class); + + private final ArrayList values = new ArrayList(); + private final MutableAnnotation mutableAnno; + private final String attribName; + + public MutableAnnotationArrayVisitor(MutableAnnotation mutableAnno, String attribName) { + super(AsmUtils.EMPTY_VISITOR); + + this.mutableAnno = mutableAnno; + this.attribName = attribName; + } + + @Override + public void visit(String na, Object value) { + values.add(value); + } + + @Override + public AnnotationVisitor visitAnnotation(String na, String annoTypeDesc) { + String annoTypeName = AsmUtils.convertTypeDescriptorToClassName(annoTypeDesc); + Class annoType = loadToolingSafeClass(annoTypeName); + + if(annoType == null) + return super.visitAnnotation(na, annoTypeDesc); + + Annotation anno = createMutableAnnotation(annoType); + values.add(anno); + return new MutableAnnotationVisitor(anno); + } + + @Override + public void visitEnd() { + Class arrayType = mutableAnno.getAttributeType(attribName); + Object[] array = (Object[])Array.newInstance(arrayType.getComponentType(), 0); + mutableAnno.setAttributeValue(attribName, values.toArray(array)); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationInvocationHandler.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationInvocationHandler.java new file mode 100644 index 0000000000..0c36007954 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationInvocationHandler.java @@ -0,0 +1,202 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import static java.lang.String.*; +import static org.springframework.core.annotation.AnnotationUtils.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + + +/** TODO: JAVADOC */ +final class MutableAnnotationInvocationHandler implements InvocationHandler { + + private final Class annoType; + private final HashMap attributes = new HashMap(); + private final HashMap> attributeTypes = new HashMap>(); + + public MutableAnnotationInvocationHandler(Class annoType) { + // pre-populate the attributes hash will all the names + // and default values of the attributes defined in 'annoType' + Method[] attribs = annoType.getDeclaredMethods(); + for(Method attrib : attribs) { + this.attributes.put(attrib.getName(), getDefaultValue(annoType, attrib.getName())); + this.attributeTypes.put(attrib.getName(), getAttributeType(annoType, attrib.getName())); + } + + this.annoType = annoType; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Assert.isInstanceOf(Annotation.class, proxy); + + String methodName = method.getName(); + + // first -> check to see if this method is an attribute on our annotation + if(attributes.containsKey(methodName)) + return attributes.get(methodName); + + + // second -> is it a method from java.lang.annotation.Annotation? + if(methodName.equals("annotationType")) + return annoType; + + + // third -> is it a method from java.lang.Object? + if(methodName.equals("toString")) + return format("@%s(%s)", annoType.getName(), getAttribs()); + + if(methodName.equals("equals")) + return isEqualTo(proxy, args[0]); + + if(methodName.equals("hashCode")) + return calculateHashCode(proxy); + + + // finally -> is it a method specified by MutableAnno? + if(methodName.equals("setAttributeValue")) { + attributes.put((String)args[0], args[1]); + return null; // setAttributeValue has a 'void' return type + } + + if(methodName.equals("getAttributeType")) + return attributeTypes.get(args[0]); + + throw new UnsupportedOperationException("this proxy does not support method: " + methodName); + } + + /** + * Conforms to the hashCode() specification for Annotation. + * + * @see Annotation#hashCode() + */ + private Object calculateHashCode(Object proxy) { + int sum = 0; + + for (String attribName : attributes.keySet()) { + Object attribValue = attributes.get(attribName); + + final int attribNameHashCode = attribName.hashCode(); + final int attribValueHashCode; + + if (attribValue == null) + // memberValue may be null when a mutable annotation is being added to a collection + // and before it has actually been visited (and populated) by MutableAnnotationVisitor + attribValueHashCode = 0; + else if (attribValue.getClass().isArray()) + attribValueHashCode = Arrays.hashCode((Object[]) attribValue); + else + attribValueHashCode = attribValue.hashCode(); + + sum += (127 * attribNameHashCode) ^ attribValueHashCode; + } + + return sum; + } + + /** + * Compares proxy object and other object by comparing the return values + * of the methods specified by their common {@link Annotation} ancestry. + *

+ * other must be the same type as or a subtype of proxy. + * Will return false otherwise. + *

+ * Eagerly returns true if {@code proxy} == other

+ *

+ * Conforms strictly to the equals() specification for Annotation

+ * + * @see Annotation#equals(Object) + */ + private Object isEqualTo(Object proxy, Object other) { + if (proxy == other) + return true; + + if (other == null) + return false; + + if(!annoType.isAssignableFrom(other.getClass())) + return false; + + for (String attribName : attributes.keySet()) { + Object thisVal; + Object thatVal; + + try { + thisVal = attributes.get(attribName); + thatVal = other.getClass().getDeclaredMethod(attribName).invoke(other); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + if ((thisVal == null) && (thatVal != null)) + return false; + + if ((thatVal == null) && (thisVal != null)) + return false; + + if (thatVal.getClass().isArray()) { + if (!Arrays.equals((Object[]) thatVal, (Object[]) thisVal)) { + return false; + } + } else if (thisVal instanceof Double) { + if (!Double.valueOf((Double) thisVal).equals(Double.valueOf((Double) thatVal))) + return false; + } else if (thisVal instanceof Float) { + if (!Float.valueOf((Float) thisVal).equals(Float.valueOf((Float) thatVal))) + return false; + } else if (!thisVal.equals(thatVal)) { + return false; + } + } + + return true; + } + + private String getAttribs() { + ArrayList attribs = new ArrayList(); + + for (String attribName : attributes.keySet()) + attribs.add(format("%s=%s", attribName, attributes.get(attribName))); + + return StringUtils.collectionToDelimitedString(attribs, ", "); + } + + /** + * Retrieve the type of the given annotation attribute. + */ + private static Class getAttributeType(Class annotationType, String attributeName) { + Method method = null; + + try { + method = annotationType.getDeclaredMethod(attributeName); + } + catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + } + + return method.getReturnType(); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationUtils.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationUtils.java new file mode 100644 index 0000000000..8b23606191 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; + + +/** TODO: JAVADOC */ +// TODO: SJC-242 made this public, revisit +public class MutableAnnotationUtils { + + /** + * Creates a {@link MutableAnnotation} for {@code annoType}. + * JDK dynamic proxies are used, and the returned proxy implements + * both {@link MutableAnnotation} and annotation type {@code A} + * + * @param annotation type that must be supplied and returned + * @param annoType type of annotation to create + */ + public static A createMutableAnnotation(Class annoType) { + MutableAnnotationInvocationHandler handler = new MutableAnnotationInvocationHandler(annoType); + ClassLoader classLoader = MutableAnnotationUtils.class.getClassLoader(); + Class[] interfaces = new Class[] {annoType, MutableAnnotation.class}; + + @SuppressWarnings("unchecked") + A mutableAnno = (A) Proxy.newProxyInstance(classLoader, interfaces, handler); + return mutableAnno; + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationVisitor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationVisitor.java new file mode 100644 index 0000000000..b652159c57 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/parsing/asm/MutableAnnotationVisitor.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.parsing.asm; + +import static org.springframework.config.java.Util.*; +import static org.springframework.config.java.internal.parsing.asm.MutableAnnotationUtils.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Type; +import org.springframework.util.Assert; + + +/** + * Populates a given {@link MutableAnnotation} instance with its attributes. + */ +class MutableAnnotationVisitor implements AnnotationVisitor { + + private static final Log log = LogFactory.getLog(MutableAnnotationVisitor.class); + + protected final MutableAnnotation mutableAnno; + + /** + * Creates a new {@link MutableAnnotationVisitor} instance that will populate + * the the attributes of the given mutableAnno. Accepts {@link Annotation} + * instead of {@link MutableAnnotation} to avoid the need for callers to typecast. + * + * @param mutableAnno {@link MutableAnnotation} instance to visit and populate + * + * @throws IllegalArgumentException if mutableAnno is not of type + * {@link MutableAnnotation} + * + * @see MutableAnnotationUtils#createMutableAnnotation(Class) + */ + public MutableAnnotationVisitor(Annotation mutableAnno) { + Assert.isInstanceOf(MutableAnnotation.class, mutableAnno, "annotation must be mutable"); + this.mutableAnno = (MutableAnnotation)mutableAnno; + } + + public AnnotationVisitor visitArray(final String attribName) { + return new MutableAnnotationArrayVisitor(mutableAnno, attribName); + } + + public void visit(String attribName, Object attribValue) { + Class attribReturnType = mutableAnno.getAttributeType(attribName); + + if (attribReturnType.equals(Class.class)) { + // the attribute type is Class -> load it and set it. + String fqClassName = ((Type) attribValue).getClassName(); + + Class classVal = loadToolingSafeClass(fqClassName); + + if(classVal == null) + return; + + mutableAnno.setAttributeValue(attribName, classVal); + return; + } + + // otherwise, assume the value can be set literally + mutableAnno.setAttributeValue(attribName, attribValue); + } + + @SuppressWarnings("unchecked") + public void visitEnum(String attribName, String enumTypeDescriptor, String strEnumValue) { + String enumClassName = AsmUtils.convertTypeDescriptorToClassName(enumTypeDescriptor); + + Class enumClass = loadToolingSafeClass(enumClassName); + + if(enumClass == null) + return; + + Enum enumValue = Enum.valueOf(enumClass, strEnumValue); + mutableAnno.setAttributeValue(attribName, enumValue); + } + + public AnnotationVisitor visitAnnotation(String attribName, String attribAnnoTypeDesc) { + String annoTypeName = AsmUtils.convertTypeDescriptorToClassName(attribAnnoTypeDesc); + Class annoType = loadToolingSafeClass(annoTypeName); + + if(annoType == null) + return AsmUtils.EMPTY_VISITOR.visitAnnotation(attribName, attribAnnoTypeDesc); + + Annotation anno = createMutableAnnotation(annoType); + + try { + Field attribute = mutableAnno.getClass().getField(attribName); + attribute.set(mutableAnno, anno); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + return new MutableAnnotationVisitor(anno); + } + + public void visitEnd() { } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/process/InternalConfigurationPostProcessor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/process/InternalConfigurationPostProcessor.java new file mode 100644 index 0000000000..20479c1035 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/internal/process/InternalConfigurationPostProcessor.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.internal.process; + +import static org.springframework.config.java.Util.*; + +import java.util.LinkedHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.internal.enhancement.CglibConfigurationEnhancer; +import org.springframework.config.java.internal.enhancement.ConfigurationEnhancer; +import org.springframework.config.java.internal.factory.support.AsmJavaConfigBeanDefinitionReader; +import org.springframework.config.java.model.ConfigurationModel; +import org.springframework.config.java.process.ConfigurationPostProcessor; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ClassUtils; + + +/** TODO: JAVADOC */ +public class InternalConfigurationPostProcessor implements BeanFactoryPostProcessor { + + private static final Log logger = LogFactory.getLog(InternalConfigurationPostProcessor.class); + + /** + * Searches beanFactory for any {@link Configuration} classes in order + * to parse and enhance them. Also registers any {@link BeanPostProcessor} objects + * necessary to fulfill JavaConfig requirements. + */ + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ConfigurationModel model = new ConfigurationModel(); + + parseAnyConfigurationClasses(beanFactory, model); + + enhanceAnyConfigurationClasses(beanFactory, model); + } + + private void parseAnyConfigurationClasses(ConfigurableListableBeanFactory beanFactory, ConfigurationModel model) { + + // linked map is important for maintaining predictable ordering of configuration classes. + // this is important in bean / value override situations. + LinkedHashMap configClassResources = new LinkedHashMap(); + + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName); + if (beanDef.isAbstract()) + continue; + + if (isConfigClass(beanDef)) { + String path = ClassUtils.convertClassNameToResourcePath(beanDef.getBeanClassName()); + configClassResources.put(beanName, new ClassPathResource(path)); + } + } + + beanDefinitionReader(beanFactory).loadBeanDefinitions(model, configClassResources); + } + + /** + * Post-processes a BeanFactory in search of Configuration class BeanDefinitions; any candidates + * are then enhanced by a {@link ConfigurationEnhancer}. Candidate status is determined by + * BeanDefinition attribute metadata. + * + * @author Chris Beams + * @see ConfigurationEnhancer + * @see BeanFactoryPostProcessor + */ + private void enhanceAnyConfigurationClasses(ConfigurableListableBeanFactory clbf, ConfigurationModel model) { + if(!(clbf instanceof DefaultListableBeanFactory)) + throw new IllegalStateException("beanFactory must be of type " + + DefaultListableBeanFactory.class.getSimpleName()); + + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)clbf; + + ConfigurationEnhancer enhancer = new CglibConfigurationEnhancer(beanFactory, model); + + int configClassesEnhanced = 0; + + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName); + + // is the beanDef marked as representing a configuration class? + if (!isConfigClass(beanDef)) + continue; + + String configClassName = beanDef.getBeanClassName(); + + String enhancedClassName = enhancer.enhance(configClassName); + + if (logger.isDebugEnabled()) + logger.debug(String.format("Replacing bean definition '%s' existing class name '%s' with enhanced class name '%s'", + beanName, configClassName, enhancedClassName)); + + beanDef.setBeanClassName(enhancedClassName); + + configClassesEnhanced++; + } + + if (configClassesEnhanced == 0) + logger.warn("Found no @Configuration class BeanDefinitions within " + beanFactory); + } + + private AsmJavaConfigBeanDefinitionReader beanDefinitionReader(ConfigurableListableBeanFactory beanFactory) { + // reader requires DefaultListableBeanFactory for it's registerBeanDefinition() method + if(!(beanFactory instanceof DefaultListableBeanFactory)) + throw new IllegalStateException("beanFactory must be of type " + + DefaultListableBeanFactory.class.getSimpleName()); + + return new AsmJavaConfigBeanDefinitionReader((DefaultListableBeanFactory)beanFactory); + } + + /** + * Determines whether the class for beanDef is a {@link Configuration}-annotated + * class. Returns false if beanDef has no class specified. + *

+ * Note: the classloading used within should not be problematic or interfere with tooling in any + * way. BeanFactoryPostProcessing happens only during actual runtime processing via + * {@link JavaConfigApplicationContext} or via XML using {@link ConfigurationPostProcessor}. In + * any case, tooling (Spring IDE) will use {@link AsmJavaConfigBeanDefinitionReader}directly, + * thus never encountering this classloading. Should this become problematic, it would not be + * too difficult to replace the following with ASM logic that traverses the class hierarchy in + * order to find whether the class is directly or indirectly annotated with + * {@link Configuration}. + */ + private static boolean isConfigClass(BeanDefinition beanDef) { + String className = beanDef.getBeanClassName(); + return className != null + && loadRequiredClass(className).isAnnotationPresent(Configuration.class); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationClass.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationClass.java new file mode 100644 index 0000000000..7dcfcae4a8 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationClass.java @@ -0,0 +1,338 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.model; + + +import static java.lang.String.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.config.java.annotation.Configuration; +import org.springframework.util.Assert; + +import sun.security.x509.Extension; + + +/** + * Abstract representation of a user-defined {@link Configuration @Configuration} class. Includes a + * set of Bean methods, AutoBean methods, ExternalBean methods, ExternalValue methods, etc. Includes + * all such methods defined in the ancestry of the class, in a 'flattened-out' manner. Note that + * each BeanMethod representation does still contain source information about where it was + * originally detected (for the purpose of tooling with Spring IDE). + * + *

Like the rest of the {@link org.springframework.config.java.model model} package, + * this class follows the fluent interface / builder pattern such that a model can be built up + * easily by method chaining.

+ * + * @author Chris Beams + */ +// TODO: SJC-242 update documentation in light of generalization changes +// consider removing all refs to Bean, ExternalBean, etc. +public final class ConfigurationClass extends ModelClass implements Validatable { + + private String beanName; + + private int modifiers; + + private Configuration metadata; + + private HashSet methods = new HashSet(); + + private HashSet pluginAnnotations = new HashSet(); + + private ConfigurationClass declaringClass; + + public ConfigurationClass() { } + + // TODO: get rid of constructors used only for testing. put in testing util. + /** + * Creates a new ConfigurationClass named className. + * + * @param name fully-qualified Configuration class being represented + * + * @see #setClassName(String) + */ + ConfigurationClass(String name) { + this(name, null, defaultAnnotation(), 0); + } + + ConfigurationClass(String name, Configuration metadata) { + this(name, null, metadata, 0); + } + + ConfigurationClass(String name, int modifiers) { + this(name, null, defaultAnnotation(), modifiers); + } + + private static Configuration defaultAnnotation() { + @Configuration class Prototype { } + return Prototype.class.getAnnotation(Configuration.class); + } + + /** + * Creates a new ConfigurationClass object. + * + * @param name Fully qualified name of the class being represented + * @param id Bean name/id (if any) of this configuration class. used only in the case + * of XML integration where {@link Configuration} beans may have a + * user-specified id. + * @param metadata Configuration annotation resident on this class. May be null indicating + * that the user specified this class to be processed but failed to properly + * annotate it. + * @param modifiers Per {@link java.lang.reflect.Modifier} + */ + public ConfigurationClass(String name, String id, Configuration metadata, int modifiers) { + super(name); + Assert.hasText(name, "Configuration class name must have text"); + + setBeanName(id); + setMetadata(metadata); + setModifiers(modifiers); + } + + public ConfigurationClass addMethod(ModelMethod method) { + method.setDeclaringClass(this); + methods.add(method); + return this; + } + + public String getBeanName() { + return beanName == null ? getName() : beanName; + } + + public ConfigurationClass setBeanName(String id) { + this.beanName = id; + return this; + } + + public Set getMethods() { + return methods; + } + + public Annotation[] getPluginAnnotations() { + return pluginAnnotations.toArray(new Annotation[pluginAnnotations.size()]); + } + + /** + * Add a {@link Extension @Plugin}-annotated annotation to this configuration class. + * + * @param pluginAnno type-level Plugin annotation + */ + public ConfigurationClass addPluginAnnotation(Annotation pluginAnno) { + pluginAnnotations.add(pluginAnno); + return this; + } + + + public ConfigurationClass setDeclaringClass(ConfigurationClass configurationClass) { + this.declaringClass = configurationClass; + return this; + } + + public ConfigurationClass getDeclaringClass() { + return declaringClass; + } + + public int getModifiers() { + return modifiers; + } + + public ConfigurationClass setModifiers(int modifiers) { + Assert.isTrue(modifiers >= 0, "modifiers must be non-negative"); + this.modifiers = modifiers; + return this; + } + + public Configuration getMetadata() { + return this.metadata; + } + + public ConfigurationClass setMetadata(Configuration configAnno) { + this.metadata = configAnno; + return this; + } + + public void validate(List errors) { + + // configuration classes must be annotated with @Configuration + if (metadata == null) + errors.add(new NonAnnotatedConfigurationError()); + + // a configuration class may not be final (CGLIB limitation) + if (Modifier.isFinal(modifiers)) + errors.add(new FinalConfigurationError()); + + for(ModelMethod method : methods) + method.validate(errors); + } + + + @Override + public String toString() { + return format("%s; modifiers=%d; methods=%s", + super.toString(), modifiers, methods); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((declaringClass == null) ? 0 : declaringClass.hashCode()); + result = prime * result + ((beanName == null) ? 0 : beanName.hashCode()); + result = prime * result + ((metadata == null) ? 0 : metadata.hashCode()); + result = prime * result + ((methods == null) ? 0 : methods.hashCode()); + result = prime * result + modifiers; + result = prime * result + ((pluginAnnotations == null) ? 0 : pluginAnnotations.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + ConfigurationClass other = (ConfigurationClass) obj; + if (declaringClass == null) { + if (other.declaringClass != null) + return false; + } else if (!declaringClass.equals(other.declaringClass)) + return false; + if (beanName == null) { + if (other.beanName != null) + return false; + } else if (!beanName.equals(other.beanName)) + return false; + if (metadata == null) { + if (other.metadata != null) + return false; + } else if (!metadata.equals(other.metadata)) + return false; + if (methods == null) { + if (other.methods != null) + return false; + } else if (!methods.equals(other.methods)) + return false; + if (modifiers != other.modifiers) + return false; + if (pluginAnnotations == null) { + if (other.pluginAnnotations != null) + return false; + } else if (!pluginAnnotations.equals(other.pluginAnnotations)) + return false; + return true; + } + + + /** Configuration classes must be annotated with {@link Configuration @Configuration}. */ + public class NonAnnotatedConfigurationError extends UsageError { + public NonAnnotatedConfigurationError() { + super(ConfigurationClass.this, -1); + } + + @Override + public String getDescription() { + return format("%s was provided as a Java Configuration class but was not annotated with @%s. " + + "Update the class definition to continue.", + getSimpleName(), Configuration.class.getSimpleName()); + } + } + + /** Configuration classes must be non-final to accommodate CGLIB subclassing. */ + public class FinalConfigurationError extends UsageError { + public FinalConfigurationError() { + super(ConfigurationClass.this, -1); + } + + @Override + public String getDescription() { + return format("@%s class may not be final. Remove the final modifier to continue.", + Configuration.class.getSimpleName()); + } + } + + + public class InvalidPluginException extends UsageError { + + private final Annotation invalidPluginAnnotation; + + public InvalidPluginException(Annotation invalidPluginAnnotation) { + super(ConfigurationClass.this, -1); + this.invalidPluginAnnotation = invalidPluginAnnotation; + } + + @Override + public String getDescription() { + return format("Annotation [%s] was not annotated with @Plugin", invalidPluginAnnotation); + } + + } + + /** + * Error raised when a Bean marked as 'allowOverriding=false' is attempted to be overridden by + * another bean definition. + * + * @see Bean#allowOverriding() + */ + public class IllegalBeanOverrideError extends UsageError { + private final ConfigurationClass authoritativeClass; + private final ModelMethod finalMethodInQuestion; + + /** + * Creates a new IllegalBeanOverrideError object. + * + * @param violatingClass class attempting an illegal override. null value signifies + * that the violating class is unknown or that there is no + * class to speak of (in the case of an XML bean definition + * doing the illegal overriding) + * @param finalMethodInQuestion the method that has been marked 'allowOverriding=false' + */ + public IllegalBeanOverrideError(ConfigurationClass violatingClass, + ModelMethod finalMethodInQuestion) { + super(violatingClass, -1); + this.authoritativeClass = ConfigurationClass.this; + this.finalMethodInQuestion = finalMethodInQuestion; + } + + @Override + public String getDescription() { + return format("Illegal attempt by '%s' to override bean definition originally " + + "specified by %s.%s. Consider removing 'allowOverride=false' from original method.", + finalMethodInQuestion.getName(), authoritativeClass.getSimpleName(), + finalMethodInQuestion.getName()); + } + } + + public boolean hasMethod(String methodName) { + return getMethod(methodName) != null; + } + + public ModelMethod getMethod(String methodName) { + + for(ModelMethod method : methods) + if(methodName.equals(method.getName())) + return method; + + return null; + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationModel.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationModel.java new file mode 100644 index 0000000000..818a6c8939 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ConfigurationModel.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.model; + +import static java.lang.String.*; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.annotation.Validator; + + +/** + * An abstract representation of a set of user-provided "Configuration classes", usually but not + * necessarily annotated with {@link Configuration @Configuration}. The model is populated with a + * {@link org.springframework.config.java.internal.parsing.ConfigurationParser} implementation which + * may be reflection-based or ASM-based. Once a model has been populated, it can then be rendered + * out to a set of BeanDefinitions. The model provides an important layer of indirection between the + * complexity of parsing a set of classes and the complexity of representing the contents of those + * classes as BeanDefinitions. + * + *

Interface follows the builder pattern for method chaining.

+ * + * @author Chris Beams + * @see org.springframework.config.java.internal.parsing.ConfigurationParser + */ +public final class ConfigurationModel implements Validatable { + + /* list is used because order and collection equality matters. */ + private final ArrayList configurationClasses = new ArrayList(); + private final ArrayList validators = new ArrayList(); + + /** + * Add a {@link Configuration @Configuration} class to the model. Classes may be added at will + * and without any particular validation. Malformed classes will be caught and errors processed + * during {@link #validate() validation} + * + * @param configurationClass user-supplied Configuration class + */ + public ConfigurationModel add(ConfigurationClass configurationClass) { + configurationClasses.add(configurationClass); + return this; + } + + public void registerValidator(Validator validator) { + validators.add(validator); + } + + /** + * Return configuration classes that have been directly added to this model. + * + * @see #getAllConfigurationClasses() + */ + public ConfigurationClass[] getConfigurationClasses() { + return configurationClasses.toArray(new ConfigurationClass[] { }); + } + +// /** +// * Return all configuration classes, including all imported configuration classes. This method +// * should be generally preferred over {@link #getConfigurationClasses()} +// * +// * @see #getConfigurationClasses() +// */ +// public ConfigurationClass[] getAllConfigurationClasses() { +// ArrayList allConfigClasses = new ArrayList(); +// +// for (ConfigurationClass configClass : configurationClasses) +// allConfigClasses.addAll(configClass.getSelfAndAllImports()); +// +// return allConfigClasses.toArray(new ConfigurationClass[allConfigClasses.size()]); +// } + + public ConfigurationClass[] getAllConfigurationClasses() { + return configurationClasses.toArray(new ConfigurationClass[configurationClasses.size()]); + } + + /** + * Recurses through the model validating each object along the way and aggregating any errors. + * + * @see ConfigurationClass#validate(java.util.List) + * @see ModelMethod#validate(java.util.List) + * @see Validator + * @see UsageError + */ + public void validate(List errors) { + // user must specify at least one configuration + if (configurationClasses.isEmpty()) + errors.add(new EmptyModelError()); + + // cascade through model and allow handlers to register validators + // depending on where they are registered (with the model, the class, or the method) + // they will be called directly or indirectly below + for (ConfigurationClass configClass : getAllConfigurationClasses()) { + for(ModelMethod method : configClass.getMethods()) { + for(Validator validator : method.getValidators()) { + if(validator.supports(method)) + method.registerValidator(validator); + // TODO: support class-level validation + // if(validator.supports(configClass)) + // configClass.registerValidator(validator); + if(validator.supports(this)) + this.registerValidator(validator); + } + } + } + + // process any validators registered directly with this model object + for(Validator validator : validators) + validator.validate(this, errors); + + // each individual configuration class must be well-formed + // note that each configClass detects usage errors on its imports recursively + // note that each configClass will recursively process its respective methods + for (ConfigurationClass configClass : configurationClasses) + configClass.validate(errors); + } + + @Override + public String toString() { + return format("%s: configurationClasses=%s", + getClass().getSimpleName(), configurationClasses); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((configurationClasses == null) ? 0 : configurationClasses.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ConfigurationModel other = (ConfigurationModel) obj; + if (configurationClasses == null) { + if (other.configurationClasses != null) + return false; + } else if (!configurationClasses.equals(other.configurationClasses)) + return false; + return true; + } + + + public class EmptyModelError extends UsageError { + public EmptyModelError() { + super(null, 0); + } + + @Override + public String getDescription() { + return format("Configuration model was empty. Make sure at least one " + + "@%s class has been specified.", Configuration.class.getSimpleName()); + } + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/MalformedJavaConfigurationException.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/MalformedJavaConfigurationException.java new file mode 100644 index 0000000000..00788515bc --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/MalformedJavaConfigurationException.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +/** + * TODO: rename to UsageException / move outside .internal? + * + * @author Chris Beams + */ +@SuppressWarnings("serial") +public class MalformedJavaConfigurationException extends RuntimeException { + + private final List errors; + + public MalformedJavaConfigurationException(String message) { + super(message); + this.errors = new ArrayList(); + } + + public MalformedJavaConfigurationException(UsageError... errors) { + super(toString(errors)); + this.errors = Arrays.asList(errors); + } + + public boolean containsError(Class errorType) { + for (UsageError error : errors) + if (error.getClass().isAssignableFrom(errorType)) + return true; + + return false; + } + + /** + * Render a list of syntax errors as output suitable for diagnosis via System.err. + */ + private static String toString(UsageError... errors) { + StringBuilder sb = new StringBuilder(); + + sb.append("\n"); + + if (errors.length == 1) + sb.append("A usage error has "); + else + sb.append(errors.length + " usage errors have "); + + sb.append("been detected:\n"); + + for (int i = 0; i < errors.length; i++) { + sb.append(errors[i].toString()); + if ((i + 1) < errors.length) + sb.append('\n'); + } + + return sb.toString(); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelClass.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelClass.java new file mode 100644 index 0000000000..f041ea412a --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelClass.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.model; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + + +/** + * Abstract representation of a class, free from java reflection. + * Base class used within the internal JavaConfig metamodel for + * representing {@link Configuration} classes. + * + * @author Chris Beams + */ +// TODO: Consider eliminating in favor of just ConfigurationClass +public class ModelClass implements BeanMetadataElement { + + private String name; + private boolean isInterface; + private String source; + + /** + * Creates a new and empty ModelClass instance. + */ + public ModelClass() { } + + /** + * Creates a new ModelClass instance + * + * @param name fully-qualified name of the class being represented + */ + public ModelClass(String name) { + this(name, false); + } + + /** + * Creates a new ModelClass instance + * + * @param name fully-qualified name of the class being represented + * @param isInterface whether the represented type is an interface + */ + public ModelClass(String name, boolean isInterface) { + this.name = name; + this.isInterface = isInterface; + } + + /** + * Returns the fully-qualified name of this class. + */ + public String getName() { + return name; + } + + /** + * Sets the fully-qualified name of this class. + */ + public void setName(String className) { + this.name = className; + } + + /** + * Returns the non-qualified name of this class. Given com.acme.Foo, returns 'Foo'. + */ + public String getSimpleName() { + return name == null ? null : ClassUtils.getShortName(name); + } + + /** + * Returns whether the class represented by this ModelClass instance is an interface. + */ + public boolean isInterface() { + return isInterface; + } + + /** + * Signifies that this class is (true) or is not (false) an interface. + */ + public void setInterface(boolean isInterface) { + this.isInterface = isInterface; + } + + /** + * Returns a resource path-formatted representation of the .java + * file that declares this class + */ + public String getSource() { + return source; + } + + /** + * Set the source location for this class. Must be a resource-path formatted string. + * + * @param source resource path to the .java file that declares this class. + */ + public void setSource(Object source) { + Assert.isInstanceOf(String.class, source); + this.source = (String) source; + } + + /** + * Given a ModelClass instance representing a class com.acme.Foo, this method will return + *
+     * ModelClass: name=Foo
+     * 
+ */ + @Override + public String toString() { + return String.format("%s: name=%s", getClass().getSimpleName(), getSimpleName()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + (isInterface ? 1231 : 1237); + result = (prime * result) + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj == null) + return false; + + if (getClass() != obj.getClass()) + return false; + + ModelClass other = (ModelClass) obj; + if (isInterface != other.isInterface) + return false; + + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + + return true; + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelMethod.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelMethod.java new file mode 100644 index 0000000000..02b3257e20 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/ModelMethod.java @@ -0,0 +1,243 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.model; + +import static java.lang.String.*; +import static org.springframework.config.java.Util.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.sf.cglib.proxy.Callback; +import net.sf.cglib.proxy.NoOp; + +import org.springframework.config.java.annotation.BeanDefinitionRegistrar; +import org.springframework.config.java.annotation.Factory; +import org.springframework.config.java.annotation.Validator; +import org.springframework.util.Assert; + + +/** TODO: JAVADOC */ +public final class ModelMethod implements Validatable { + + private final String name; + private final int modifiers; + private final ModelClass returnType; + private final List annotations = new ArrayList(); + private transient ConfigurationClass declaringClass; + private transient int lineNumber; + private transient Factory factoryAnno; + private transient final List validators = new ArrayList(); + + public ModelMethod(String name, int modifiers, ModelClass returnType, Annotation... annotations) { + Assert.hasText(name); + this.name = name; + + Assert.notNull(annotations); + for(Annotation annotation : annotations) { + this.annotations.add(annotation); + if(factoryAnno == null) + factoryAnno = annotation.annotationType().getAnnotation(Factory.class); + } + + Assert.isTrue(modifiers >= 0, "modifiers must be non-negative: " + modifiers); + this.modifiers = modifiers; + + Assert.notNull(returnType); + this.returnType = returnType; + } + + public String getName() { + return name; + } + + public ModelClass getReturnType() { + return returnType; + } + + /** + * @see java.lang.reflect.Modifier + */ + public int getModifiers() { + return modifiers; + } + + /** + * Returns the annotation on this method matching annoType or null + * IllegalStateException} if not present. + * + * @see #getRequiredAnnotation(Class) + */ + @SuppressWarnings("unchecked") + public T getAnnotation(Class annoType) { + for(Annotation anno : annotations) + if(anno.annotationType().equals(annoType)) + return (T) anno; + + return null; + } + + /** + * Returns the annotation on this method matching annoType or throws + * {@link IllegalStateException} if not present. + * + * @see #getAnnotation(Class) + */ + public T getRequiredAnnotation(Class annoType) { + T anno = getAnnotation(annoType); + + if(anno == null) + throw new IllegalStateException( + format("annotation %s not found on %s", annoType.getSimpleName(), this)); + + return anno; + } + + /** + * Sets up bi-directional relationship between this method and its declaring class. + * + * @see ConfigurationClass#addMethod(ModelMethod) + */ + public void setDeclaringClass(ConfigurationClass declaringClass) { + this.declaringClass = declaringClass; + } + + public ConfigurationClass getDeclaringClass() { + return declaringClass; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public int getLineNumber() { + return lineNumber; + } + + public void registerValidator(Validator validator) { + validators.add(validator); + } + + public void validate(List errors) { + for(Validator validator : validators) + validator.validate(this, errors); + + if (Modifier.isPrivate(getModifiers())) + errors.add(new PrivateMethodError()); + + if (Modifier.isFinal(getModifiers())) + errors.add(new FinalMethodError()); + } + + public BeanDefinitionRegistrar getRegistrar() { + return getInstance(factoryAnno.registrarType()); + } + + public Set getValidators() { + HashSet validator = new HashSet(); + + for(Class validatorType : factoryAnno.validatorTypes()) + validator.add(getInstance(validatorType)); + + return validator; + } + + public Callback getCallback() { + Class callbackType = factoryAnno.callbackType(); + + if(callbackType.equals(NoOp.class)) + return NoOp.INSTANCE; + + return getInstance(callbackType); + } + + @Override + public String toString() { + String returnTypeName = returnType == null ? "" : returnType.getSimpleName(); + return String.format("%s: name=%s; returnType=%s; modifiers=%d", + getClass().getSimpleName(), name, returnTypeName, modifiers); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((annotations == null) ? 0 : annotations.hashCode()); + result = prime * result + modifiers; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((returnType == null) ? 0 : returnType.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ModelMethod other = (ModelMethod) obj; + if (annotations == null) { + if (other.annotations != null) + return false; + } else if (!annotations.equals(other.annotations)) + return false; + if (modifiers != other.modifiers) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (returnType == null) { + if (other.returnType != null) + return false; + } else if (!returnType.equals(other.returnType)) + return false; + return true; + } + + /** JavaConfigMethods must be visible (non-private) in order to accommodate CGLIB. */ + public class PrivateMethodError extends UsageError { + public PrivateMethodError() { + super(getDeclaringClass(), getLineNumber()); + } + + @Override + public String getDescription() { + return format("method '%s' may not be private", getName()); + } + } + + /** JavaConfigMethods must be extensible (non-final) in order to accommodate CGLIB. */ + public class FinalMethodError extends UsageError { + public FinalMethodError() { + super(getDeclaringClass(), getLineNumber()); + } + + @Override + public String getDescription() { + return format("method '%s' may not be final - remove the final modifier to continue", + getName()); + } + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/UsageError.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/UsageError.java new file mode 100644 index 0000000000..c431f46828 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/UsageError.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.model; + +import org.springframework.config.java.annotation.Configuration; + + +/** + * Represents an invalid usage of JavaConfig constructs, e.g. a {@link Configuration} that declares + * no {@link Bean @Bean} methods, or declaring both {@link Bean @Bean} and + * {@link ExternalBean @ExternalBean} on a single method. Explore the type hierarchy to discover all + * possible usage errors. + * + * @author Chris Beams + * @see MalformedJavaConfigurationException + */ +public abstract class UsageError { + + private final ModelClass clazz; + private final int lineNumber; + + /** + * Create a new usage error, providing information about where the error was detected. + * + * @param modelClass class in which this error was detected. Null value indicates that the + * error was not local to a single class. + * @param lineNumber line number on which this error was detected (useful for tooling integration) + * + * @see ModelClass#getSource() + */ + public UsageError(ModelClass modelClass, int lineNumber) { + this.clazz = modelClass; + this.lineNumber = lineNumber; + } + + /** + * Human-readable description of this error suitable for console output or IDE tooling. + */ + public abstract String getDescription(); + + /** + * Same as {@link #getDescription()} but attributed with class and line number information. If + * modelClass constructor parameter was null, class and line number information will be omitted. + */ + public final String getAttributedDescription() { + if (clazz == null) + return getDescription(); + + return String.format("%s:%d: %s", clazz.getSource(), lineNumber, getDescription()); + } + + /** + * Delegates directly to {@link #getAttributedDescription()}. + */ + @Override + public String toString() { + return getAttributedDescription(); + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/model/Validatable.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/Validatable.java new file mode 100644 index 0000000000..6d0397e113 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/model/Validatable.java @@ -0,0 +1,19 @@ +package org.springframework.config.java.model; + +import java.util.List; + +/** + * Indicates a type is able to be validated for errors. + * + * @see Validator + * + * @author Chris Beams + */ +public interface Validatable { + + /** + * Validates this object, adding any errors to the supplied list of errors. + */ + public void validate(List errors); + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/Extension.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/Extension.java new file mode 100644 index 0000000000..928c9037a4 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/Extension.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.plugin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.Ordered; + + +/** + * TODO: JAVADOC + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +@Inherited +public @interface Extension { + /** + * The class that handles this plugin. + */ + // TODO: SJC-242 rename to handlerType / handlerClass + Class> handler(); + + /** + * The order in which this plugin will be processed + * relative to others. Per the semantics of {@link Ordered}, + * lower integer values will be treated as higher priority. + * + * @see Ordered + */ + int order() default 0; +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/ExtensionAnnotationBeanDefinitionRegistrar.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/ExtensionAnnotationBeanDefinitionRegistrar.java new file mode 100644 index 0000000000..22cd1a4416 --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/plugin/ExtensionAnnotationBeanDefinitionRegistrar.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.plugin; + +import java.lang.annotation.Annotation; + +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + + +/** + * Registers bean definitions based on {@link Extension} metadata + */ +public interface ExtensionAnnotationBeanDefinitionRegistrar
{ + + /** + * + */ + void handle(A annotation, BeanDefinitionRegistry registry); +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/process/ConfigurationPostProcessor.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/process/ConfigurationPostProcessor.java new file mode 100644 index 0000000000..d763025e6c --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/process/ConfigurationPostProcessor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.process; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.internal.process.InternalConfigurationPostProcessor; +import org.springframework.core.Ordered; + + +/** + * {@link BeanFactoryPostProcessor} used for bootstrapping {@link Configuration @Configuration} + * beans from Spring XML files. + */ +// TODO: This class now just delegates to InternalConfigurationPostProcessor. Eliminate? +public class ConfigurationPostProcessor implements Ordered, BeanFactoryPostProcessor { + + /** + * Iterates through beanFactory, detecting and processing any {@link Configuration} + * bean definitions. + */ + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + new InternalConfigurationPostProcessor().postProcessBeanFactory(beanFactory); + } + + /** + * Returns the order in which this {@link BeanPostProcessor} will be executed. + * Returns {@link Ordered#HIGHEST_PRECEDENCE}. + */ + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + +} diff --git a/org.springframework.config.java/src/main/java/org/springframework/config/java/util/DefaultScopes.java b/org.springframework.config.java/src/main/java/org/springframework/config/java/util/DefaultScopes.java new file mode 100644 index 0000000000..f92c2ced9e --- /dev/null +++ b/org.springframework.config.java/src/main/java/org/springframework/config/java/util/DefaultScopes.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.config.java.util; + +import org.springframework.beans.factory.config.BeanDefinition; + + +/** + * Constant class contains the names of the scopes supported out of the box in Spring 2.0. + * + * @author Costin Leau + * @author Chris Beams + */ +public abstract class DefaultScopes { + + public static final String SINGLETON = BeanDefinition.SCOPE_SINGLETON; + + public static final String PROTOTYPE = BeanDefinition.SCOPE_PROTOTYPE; + + public static final String REQUEST = "request"; // see WebApplicationContext.SCOPE_REQUEST; + + public static final String SESSION = "session"; // see WebApplicationContext.SCOPE_SESSION; + +} diff --git a/org.springframework.config.java/src/test/java/test/basic/BasicTests.java b/org.springframework.config.java/src/test/java/test/basic/BasicTests.java new file mode 100644 index 0000000000..be29f00bf8 --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/basic/BasicTests.java @@ -0,0 +1,110 @@ +package test.basic; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.beans.factory.support.BeanDefinitionBuilder.*; + +import org.junit.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.config.java.annotation.Bean; +import org.springframework.config.java.annotation.Configuration; +import org.springframework.config.java.process.ConfigurationPostProcessor; +import org.springframework.config.java.util.DefaultScopes; + +import test.beans.ITestBean; +import test.beans.TestBean; + +public class BasicTests { + @Test + public void test() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("config", rootBeanDefinition(Config.class).getBeanDefinition()); + + Config config = factory.getBean("config", Config.class); + assertThat(config, notNullValue()); + } + + @Test + public void test2() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("config", rootBeanDefinition(Config.class).getBeanDefinition()); + + BeanFactoryPostProcessor bfpp = new MyPostProcessor(); + + bfpp.postProcessBeanFactory(factory); + + OtherConfig config = factory.getBean("config", OtherConfig.class); + assertThat(config, notNullValue()); + } + + @Test + public void test3() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("config", rootBeanDefinition(Config.class).getBeanDefinition()); + + new ConfigurationPostProcessor().postProcessBeanFactory(factory); + + String stringBean = factory.getBean("stringBean", String.class); + + assertThat(stringBean, equalTo("foo")); + } + + @Test + public void test4() { + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("config", rootBeanDefinition(Config2.class).getBeanDefinition()); + + new ConfigurationPostProcessor().postProcessBeanFactory(factory); + + TestBean foo = factory.getBean("foo", TestBean.class); + ITestBean bar = factory.getBean("bar", ITestBean.class); + ITestBean baz = factory.getBean("baz", ITestBean.class); + + assertThat(foo.getSpouse(), sameInstance(bar)); + assertThat(bar.getSpouse(), not(sameInstance(baz))); + } +} + +class MyPostProcessor implements BeanFactoryPostProcessor { + + public void postProcessBeanFactory( + ConfigurableListableBeanFactory beanFactory) throws BeansException { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition("config"); + beanDefinition.setBeanClassName(OtherConfig.class.getName()); + } + +} + +@Configuration +class Config { + public @Bean String stringBean() { + return "foo"; + } +} + +@Configuration +class Config2 { + public @Bean TestBean foo() { + TestBean foo = new TestBean("foo"); + foo.setSpouse(bar()); + return foo; + } + + public @Bean TestBean bar() { + TestBean bar = new TestBean("bar"); + bar.setSpouse(baz()); + return bar; + } + + @Bean(scope=DefaultScopes.PROTOTYPE) + public TestBean baz() { + return new TestBean("bar"); + } +} + +class OtherConfig { +} diff --git a/org.springframework.config.java/src/test/java/test/beans/Colour.java b/org.springframework.config.java/src/test/java/test/beans/Colour.java new file mode 100644 index 0000000000..225e1d7d16 --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/Colour.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + +import org.springframework.core.enums.ShortCodedLabeledEnum; + + +/** + * TODO: JAVADOC + * + * @author Rob Harrop + */ +@SuppressWarnings("serial") +public class Colour extends ShortCodedLabeledEnum { + + public static final Colour RED = new Colour(0, "RED"); + + public static final Colour BLUE = new Colour(1, "BLUE"); + + public static final Colour GREEN = new Colour(2, "GREEN"); + + public static final Colour PURPLE = new Colour(3, "PURPLE"); + + private Colour(int code, String label) { super(code, label); } +} diff --git a/org.springframework.config.java/src/test/java/test/beans/DependsOnTestBean.java b/org.springframework.config.java/src/test/java/test/beans/DependsOnTestBean.java new file mode 100644 index 0000000000..f743db5e9c --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/DependsOnTestBean.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + + +/** TODO: JAVADOC */ +public class DependsOnTestBean { + public TestBean tb; + + private int state; + + public void setTestBean(TestBean tb) { this.tb = tb; } + + public int getState() { return state; } + + public TestBean getTestBean() { return tb; } + +} diff --git a/org.springframework.config.java/src/test/java/test/beans/INestedTestBean.java b/org.springframework.config.java/src/test/java/test/beans/INestedTestBean.java new file mode 100644 index 0000000000..e87242df13 --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/INestedTestBean.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + + +/** TODO: JAVADOC */ +public interface INestedTestBean { + + String getCompany(); + +} diff --git a/org.springframework.config.java/src/test/java/test/beans/IOther.java b/org.springframework.config.java/src/test/java/test/beans/IOther.java new file mode 100644 index 0000000000..4a3da3dc89 --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/IOther.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + + +/** TODO: JAVADOC */ +public interface IOther { + + void absquatulate(); + +} diff --git a/org.springframework.config.java/src/test/java/test/beans/ITestBean.java b/org.springframework.config.java/src/test/java/test/beans/ITestBean.java new file mode 100644 index 0000000000..2e716120a6 --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/ITestBean.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + +import java.io.IOException; + + +/** + * Interface used for test beans. Two methods are the same as on Person, but if + * this extends person it breaks quite a few tests + * + * @author Rod Johnson + */ +public interface ITestBean { + + int getAge(); + + void setAge(int age); + + String getName(); + + void setName(String name); + + ITestBean getSpouse(); + + void setSpouse(ITestBean spouse); + + /** + * t null no error. + */ + void exceptional(Throwable t) throws Throwable; + + Object returnsThis(); + + INestedTestBean getDoctor(); + + INestedTestBean getLawyer(); + + IndexedTestBean getNestedIndexedBean(); + + /** + * Increment the age by one. + * + * @return the previous age + */ + int haveBirthday(); + + void unreliableFileOperation() throws IOException; +} diff --git a/org.springframework.config.java/src/test/java/test/beans/IndexedTestBean.java b/org.springframework.config.java/src/test/java/test/beans/IndexedTestBean.java new file mode 100644 index 0000000000..e3bb777f4f --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/IndexedTestBean.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; + + +/** + * TODO: JAVADOC + * + * @author Juergen Hoeller + * @since 11.11.2003 + */ +public class IndexedTestBean { + + private TestBean[] array; + + private Collection collection; + + private List list; + + private Set set; + + private SortedSet sortedSet; + + private Map map; + + private SortedMap sortedMap; + + public IndexedTestBean() { this(true); } + + public IndexedTestBean(boolean populate) { + if (populate) { + populate(); + } + } + + public void populate() { + TestBean tb0 = new TestBean("name0", 0); + TestBean tb1 = new TestBean("name1", 0); + TestBean tb2 = new TestBean("name2", 0); + TestBean tb3 = new TestBean("name3", 0); + TestBean tb4 = new TestBean("name4", 0); + TestBean tb5 = new TestBean("name5", 0); + TestBean tb6 = new TestBean("name6", 0); + TestBean tb7 = new TestBean("name7", 0); + TestBean tbX = new TestBean("nameX", 0); + TestBean tbY = new TestBean("nameY", 0); + this.array = new TestBean[] { tb0, tb1 }; + this.list = new ArrayList(); + this.list.add(tb2); + this.list.add(tb3); + this.set = new TreeSet(); + this.set.add(tb6); + this.set.add(tb7); + this.map = new HashMap(); + this.map.put("key1", tb4); + this.map.put("key2", tb5); + this.map.put("key.3", tb5); + List list = new ArrayList(); + list.add(tbX); + list.add(tbY); + this.map.put("key4", list); + } + + public TestBean[] getArray() { return array; } + + public void setArray(TestBean[] array) { this.array = array; } + + public Collection getCollection() { return collection; } + + public void setCollection(Collection collection) { this.collection = collection; } + + public List getList() { return list; } + + public void setList(List list) { this.list = list; } + + public Set getSet() { return set; } + + public void setSet(Set set) { this.set = set; } + + public SortedSet getSortedSet() { return sortedSet; } + + public void setSortedSet(SortedSet sortedSet) { this.sortedSet = sortedSet; } + + public Map getMap() { return map; } + + public void setMap(Map map) { this.map = map; } + + public SortedMap getSortedMap() { return sortedMap; } + + public void setSortedMap(SortedMap sortedMap) { this.sortedMap = sortedMap; } + +} diff --git a/org.springframework.config.java/src/test/java/test/beans/NestedTestBean.java b/org.springframework.config.java/src/test/java/test/beans/NestedTestBean.java new file mode 100644 index 0000000000..a9142f25d1 --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/NestedTestBean.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + +/** + * Simple nested test bean used for testing bean factories, AOP framework etc. + * + * @author Trevor D. Cook + * @since 30.09.2003 + */ +public class NestedTestBean implements INestedTestBean { + + private String company = ""; + + public NestedTestBean() { } + + public NestedTestBean(String company) { + setCompany(company); + } + + public void setCompany(String company) { + this.company = ((company != null) ? company : ""); + } + + public String getCompany() { + return company; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NestedTestBean)) { + return false; + } + + NestedTestBean ntb = (NestedTestBean) obj; + return this.company.equals(ntb.company); + } + + @Override + public int hashCode() { + return this.company.hashCode(); + } + + @Override + public String toString() { + return "NestedTestBean: " + this.company; + } + +} diff --git a/org.springframework.config.java/src/test/java/test/beans/TestBean.java b/org.springframework.config.java/src/test/java/test/beans/TestBean.java new file mode 100644 index 0000000000..027e18364e --- /dev/null +++ b/org.springframework.config.java/src/test/java/test/beans/TestBean.java @@ -0,0 +1,295 @@ +/* + * Copyright 2002-2008 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 test.beans; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; + +import org.springframework.util.ObjectUtils; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + + +/** + * Simple test bean used for testing bean factories, AOP framework etc. + * + * @author Rod Johnson + * @since 15 April 2001 + */ +public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOther, Comparable { + + private String beanName; + + private String country; + + private BeanFactory beanFactory; + + private boolean postProcessed; + + private String name; + + private String sex; + + private int age; + + private boolean jedi; + + private ITestBean spouse; + + private String touchy; + + private String[] stringArray; + + private Integer[] someIntegerArray; + + private Date date = new Date(); + + private Float myFloat = new Float(0.0); + + private Collection friends = new LinkedList(); + + private Set someSet = new HashSet(); + + private Map someMap = new HashMap(); + + private List someList = new ArrayList(); + + private Properties someProperties = new Properties(); + + private INestedTestBean doctor = new NestedTestBean(); + + private INestedTestBean lawyer = new NestedTestBean(); + + private IndexedTestBean nestedIndexedBean; + + private boolean destroyed; + + private Number someNumber; + + private Colour favouriteColour; + + private Boolean someBoolean; + + private List otherColours; + + private List pets; + + public TestBean() { } + + public TestBean(String name) { this.name = name; } + + public TestBean(ITestBean spouse) { this.spouse = spouse; } + + public TestBean(String name, int age) { + this.name = name; + this.age = age; + } + + public TestBean(ITestBean spouse, Properties someProperties) { + this.spouse = spouse; + this.someProperties = someProperties; + } + + public void setBeanName(String beanName) { this.beanName = beanName; } + + public String getBeanName() { return beanName; } + + public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } + + public BeanFactory getBeanFactory() { return beanFactory; } + + public void setPostProcessed(boolean postProcessed) { this.postProcessed = postProcessed; } + + public boolean isPostProcessed() { return postProcessed; } + + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + public String getSex() { return sex; } + + public void setSex(String sex) { this.sex = sex; } + + public int getAge() { return age; } + + public void setAge(int age) { this.age = age; } + + public boolean isJedi() { return jedi; } + + public void setJedi(boolean jedi) { this.jedi = jedi; } + + public ITestBean getSpouse() { return spouse; } + + public void setSpouse(ITestBean spouse) { this.spouse = spouse; } + + public String getTouchy() { return touchy; } + + public void setTouchy(String touchy) throws Exception { + if (touchy.indexOf('.') != -1) { + throw new Exception("Can't contain a ."); + } + + if (touchy.indexOf(',') != -1) { + throw new NumberFormatException("Number format exception: contains a ,"); + } + + this.touchy = touchy; + } + + public String getCountry() { return country; } + + public void setCountry(String country) { this.country = country; } + + public String[] getStringArray() { return stringArray; } + + public void setStringArray(String[] stringArray) { this.stringArray = stringArray; } + + public Integer[] getSomeIntegerArray() { return someIntegerArray; } + + public void setSomeIntegerArray(Integer[] someIntegerArray) { this.someIntegerArray = someIntegerArray; } + + public Date getDate() { return date; } + + public void setDate(Date date) { this.date = date; } + + public Float getMyFloat() { return myFloat; } + + public void setMyFloat(Float myFloat) { this.myFloat = myFloat; } + + public Collection getFriends() { return friends; } + + public void setFriends(Collection friends) { this.friends = friends; } + + public Set getSomeSet() { return someSet; } + + public void setSomeSet(Set someSet) { this.someSet = someSet; } + + public Map getSomeMap() { return someMap; } + + public void setSomeMap(Map someMap) { this.someMap = someMap; } + + public List getSomeList() { return someList; } + + public void setSomeList(List someList) { this.someList = someList; } + + public Properties getSomeProperties() { return someProperties; } + + public void setSomeProperties(Properties someProperties) { this.someProperties = someProperties; } + + public INestedTestBean getDoctor() { return doctor; } + + public INestedTestBean getLawyer() { return lawyer; } + + public void setDoctor(INestedTestBean bean) { doctor = bean; } + + public void setLawyer(INestedTestBean bean) { lawyer = bean; } + + public Number getSomeNumber() { return someNumber; } + + public void setSomeNumber(Number someNumber) { this.someNumber = someNumber; } + + public Colour getFavouriteColour() { return favouriteColour; } + + public void setFavouriteColour(Colour favouriteColour) { this.favouriteColour = favouriteColour; } + + public Boolean getSomeBoolean() { return someBoolean; } + + public void setSomeBoolean(Boolean someBoolean) { this.someBoolean = someBoolean; } + + public IndexedTestBean getNestedIndexedBean() { return nestedIndexedBean; } + + public void setNestedIndexedBean(IndexedTestBean nestedIndexedBean) { this.nestedIndexedBean = nestedIndexedBean; } + + public List getOtherColours() { return otherColours; } + + public void setOtherColours(List otherColours) { this.otherColours = otherColours; } + + public List getPets() { return pets; } + + public void setPets(List pets) { this.pets = pets; } + + /** + * @see ITestBean#exceptional(Throwable) + */ + public void exceptional(Throwable t) throws Throwable { + if (t != null) { + throw t; + } + } + + public void unreliableFileOperation() throws IOException { throw new IOException(); } + + /** + * @see ITestBean#returnsThis() + */ + public Object returnsThis() { return this; } + + /** + * @see IOther#absquatulate() + */ + public void absquatulate() { } + + public int haveBirthday() { return age++; } + + public void destroy() { this.destroyed = true; } + + public boolean wasDestroyed() { return destroyed; } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if ((other == null) || !(other instanceof TestBean)) { + return false; + } + + TestBean tb2 = (TestBean) other; + return (ObjectUtils.nullSafeEquals(this.name, tb2.name) && (this.age == tb2.age)); + } + + @Override + public int hashCode() { return this.age; } + + public int compareTo(Object other) { + if ((this.name != null) && (other instanceof TestBean)) { + return this.name.compareTo(((TestBean) other).getName()); + } + + return 1; + } + + @Override + public String toString() { + String s = "name=" + name + "; age=" + age + "; touchy=" + touchy; + s += "; spouse={" + ((spouse != null) ? spouse.getName() : null) + "}"; + return s; + } + +} diff --git a/org.springframework.config.java/template.mf b/org.springframework.config.java/template.mf new file mode 100644 index 0000000000..0536888878 --- /dev/null +++ b/org.springframework.config.java/template.mf @@ -0,0 +1,24 @@ +Bundle-SymbolicName: org.springframework.config.java +Bundle-Name: Spring JavaConfig +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + org.springframework.core.*;version="[2.5.4.A, 3.0.0)", + org.springframework.beans.*;version="[2.5.4.A, 3.0.0)", + org.springframework.context.*;version="[2.5.4.A, 3.0.0)", + org.springframework.transaction.interceptor.*;version="[2.5.4.A, 3.0.0)", + org.springframework.asm.*;version="[2.5.4.A, 3.0.0)", + org.springframework.aop.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.stereotype.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.util.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.test.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.web.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.jmx.export.annotation.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.jmx.support.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.springframework.jndi.*;version="[2.5.4.A, 3.0.0)";resolution:=optional, + org.aspectj.*;version="[1.6.2, 2.0.0)";resolution:=optional, + org.apache.commons.logging;version="[1.1.1, 2.0.0)", + org.aopalliance.*;version="[1.0.0,2.0.0)", + net.sf.cglib.*;version="[2.1.3, 3.0.0)" +Exclude-Exports: + org.springframework.config.java.internal.*