/******************************************************************************* * Copyright (c) 2009, 2013 Spring IDE Developers * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Spring IDE Developers - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.beans.core.autowire.internal.provider; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import javax.xml.ws.Service; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.ide.eclipse.beans.core.autowire.AutowireBeanReference; import org.springframework.ide.eclipse.beans.core.autowire.IAutowireDependencyResolver; import org.springframework.ide.eclipse.beans.core.internal.model.BeansModelUtils; import org.springframework.ide.eclipse.beans.core.model.IBean; import org.springframework.ide.eclipse.beans.core.model.IBeanReference; import org.springframework.ide.eclipse.beans.core.model.IBeansModelElement; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** * {@link IInjectionMetadataProvider} implementation that supports common Java annotations out of the box, in particular * the JSR-250 annotations in the <code>javax.annotation</code> package. These common Java annotations are supported in * many Java EE 5 technologies (e.g. JSF 1.2), as well as in Java 6's JAX-WS. * <p> * The central element is the {@link javax.annotation.Resource} annotation for annotation-driven injection of named * beans, by default from the containing Spring BeanFactory, with only <code>mappedName</code> references resolved in * JNDI. The {@link #setAlwaysUseJndiLookup "alwaysUseJndiLookup" flag} enforces JNDI lookups equivalent to standard * Java EE 5 resource injection for <code>name</code> references and default names as well. The target beans can be * simple POJOs, with no special requirements other than the type having to match. * <p> * The JAX-WS {@link javax.xml.ws.WebServiceRef} annotation is supported too, analogous to * {@link javax.annotation.Resource} but with the capability of creating specific JAX-WS service endpoints. This may * either point to an explicitly defined resource by name or operate on a locally specified JAX-WS service class. * Finally, this post-processor also supports the EJB 3 {@link javax.ejb.EJB} annotation, analogous to * {@link javax.annotation.Resource} as well, with the capability to specify both a local bean name and a global JNDI * name for fallback retrieval. The target beans can be plain POJOs as well as EJB 3 Session Beans in this case. * <p> * The common annotations supported by this post-processor are available in Java 6 (JDK 1.6) as well as in Java EE 5 * (which provides a standalone jar for its common annotations as well, allowing for use in any Java 5 based * application). Hence, this post-processor works out of the box on JDK 1.6, and requires the JSR-250 API jar (and * optionally the JAX-WS API jar and/or the EJB 3 API jar) to be added to the classpath on JDK 1.5 (when running outside * of Java EE 5). * <p> * For default usage, resolving resource names as Spring bean names, simply define the following in your application * context: * <pre class="code"> * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/> * </pre> * For direct JNDI access, resolving resource names as JNDI resource references within the Java EE application's * "java:comp/env/" namespace, use the following: * <pre class="code"> * <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"> * <property name="alwaysUseJndiLookup" value="true"/> * </bean> * </pre> * <code>mappedName</code> references will always be resolved in JNDI, allowing for global JNDI names (including "java:" * prefix) as well. The "alwaysUseJndiLookup" flag just affects <code>name</code> references and default names (inferred * from the field name / property name). * <p> * <b>NOTE:</b> A default CommonAnnotationBeanPostProcessor will be registered by the "context:annotation-config" and * "context:component-scan" XML tags. Remove or turn off the default annotation configuration there if you intend to * specify a custom CommonAnnotationBeanPostProcessor bean definition! * @author Christian Dupuis * @author Juergen Hoeller * @since 2.2.7 */ public class CommonAnnnotationInjectionMetadataProvider implements IInjectionMetadataProvider { private static String webServiceRefClassName = "javax.xml.ws.WebServiceRef"; private static String ejbRefClassName = "javax.ejb.EJB"; private static String resourceClassName = "javax.annotation.Resource"; private final Set<String> ignoredResourceTypes = new HashSet<String>(1); private boolean fallbackToDefaultTypeMatch = true; private IInjectionMetadataProviderProblemReporter problemReporter = new PassThroughProblemReporter(); private transient final Map<Class<?>, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<Class<?>, InjectionMetadata>(); public CommonAnnnotationInjectionMetadataProvider() { ignoreResourceType("javax.xml.ws.WebServiceContext"); } /** * Set the internally {@link IInjectionMetadataProviderProblemReporter}. */ public void setProblemReporter(IInjectionMetadataProviderProblemReporter problemReporter) { this.problemReporter = problemReporter; } /** * Ignore the given resource type when resolving <code>@Resource</code> annotations. * <p> * By default, the <code>javax.xml.ws.WebServiceContext</code> interface will be ignored, since it will be resolved * by the JAX-WS runtime. * @param resourceType the resource type to ignore */ public void ignoreResourceType(String resourceType) { Assert.notNull(resourceType, "Ignored resource type must not be null"); this.ignoredResourceTypes.add(resourceType); } /** * {@inheritDoc} */ public InjectionMetadata findAutowiringMetadata(final Class<?> clazz) { // Quick check on the concurrent map first, with minimal locking. InjectionMetadata metadata = this.injectionMetadataCache.get(clazz); if (metadata == null) { synchronized (this.injectionMetadataCache) { Class<? extends Annotation> annotationClass = null; // Load the annotation classes in the same class loader context as the user clazz try { annotationClass = (Class<? extends Annotation>) clazz.getClassLoader().loadClass(resourceClassName); } catch (ClassNotFoundException e) { annotationClass = null; } final Class<? extends Annotation> resourceClass = annotationClass; try { annotationClass = (Class<? extends Annotation>) clazz.getClassLoader().loadClass( webServiceRefClassName); } catch (ClassNotFoundException e) { annotationClass = null; } final Class<? extends Annotation> webServiceRefClass = annotationClass; try { annotationClass = (Class<? extends Annotation>) clazz.getClassLoader().loadClass(ejbRefClassName); } catch (ClassNotFoundException e) { annotationClass = null; } final Class<? extends Annotation> ejbRefClass = annotationClass; metadata = this.injectionMetadataCache.get(clazz); if (metadata == null) { final InjectionMetadata newMetadata = new InjectionMetadata(); ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() { public void doWith(Field field) { if (webServiceRefClass != null && field.isAnnotationPresent(webServiceRefClass)) { if (Modifier.isStatic(field.getModifiers())) { problemReporter.error( "@WebServiceRef annotation is not supported on static fields", field); return; } newMetadata.addInjectedField(new WebServiceRefElement(field, null, webServiceRefClass)); } else if (ejbRefClass != null && field.isAnnotationPresent(ejbRefClass)) { if (Modifier.isStatic(field.getModifiers())) { problemReporter.error("@EJB annotation is not supported on static fields", field); return; } newMetadata.addInjectedField(new EjbRefElement(field, null, ejbRefClass)); } else if (field.isAnnotationPresent(resourceClass)) { if (Modifier.isStatic(field.getModifiers())) { problemReporter.error("@Resource annotation is not supported on static fields", field); return; } if (!ignoredResourceTypes.contains(field.getType().getName())) { newMetadata.addInjectedField(new ResourceElement(field, null, resourceClass)); } } } }); ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() { public void doWith(Method method) { if (webServiceRefClass != null && method.isAnnotationPresent(webServiceRefClass) && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { problemReporter.error( "@WebServiceRef annotation is not supported on static methods", method); return; } if (method.getParameterTypes().length != 1) { problemReporter.error("@WebServiceRef annotation requires a single-arg method", method); return; } PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); newMetadata.addInjectedMethod(new WebServiceRefElement(method, pd, webServiceRefClass)); } else if (ejbRefClass != null && method.isAnnotationPresent(ejbRefClass) && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { problemReporter.error("@EJB annotation is not supported on static methods", method); return; } if (method.getParameterTypes().length != 1) { problemReporter.error("@EJB annotation requires a single-arg method", method); return; } PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); newMetadata.addInjectedMethod(new EjbRefElement(method, pd, ejbRefClass)); } else if (method.isAnnotationPresent(resourceClass) && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { problemReporter.error("@Resource annotation is not supported on static methods", method); return; } Class<?>[] paramTypes = method.getParameterTypes(); if (paramTypes.length != 1) { problemReporter.error("@Resource annotation requires a single-arg method", method); return; } if (!ignoredResourceTypes.contains(paramTypes[0].getName())) { PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); newMetadata.addInjectedMethod(new ResourceElement(method, pd, resourceClass)); } } } }); metadata = newMetadata; this.injectionMetadataCache.put(clazz, metadata); } } } return metadata; } /** * Class<?> representing generic injection information about an annotated field or setter method, supporting @Resource * and related annotations. */ protected abstract class LookupElement extends InjectionMetadata.InjectedElement { protected String name; protected boolean isDefaultName = false; protected Class<?> lookupType; protected String mappedName; public LookupElement(Member member, PropertyDescriptor pd, Class<? extends Annotation> annotationClass) { super(member, pd); initAnnotation((AnnotatedElement) member, annotationClass); } protected abstract void initAnnotation(AnnotatedElement ae, Class<? extends Annotation> annotationClass); /** * Return the resource name for the lookup. */ public final String getName() { return this.name; } /** * Return the desired type for the lookup. */ public final Class<?> getLookupType() { return this.lookupType; } public Set<IBeanReference> getBeanReferences(IBean bean, IBeansModelElement context, IAutowireDependencyResolver resolver) { BeanDefinition bd = BeansModelUtils.getMergedBeanDefinition(bean, context); if (!shouldSkip(bd)) { if (fallbackToDefaultTypeMatch && isDefaultName && !resolver.containsBean(name)) { return super.getBeanReferences(bean, context, resolver); } else { String[] matchingBeans = resolver.getBeansForType(lookupType); for (String matchingBen : matchingBeans) { if (name.equals(matchingBen) || Arrays.asList(resolver.getAliases(matchingBen)).contains(name)) { IBeanReference ref = new AutowireBeanReference(bean, new RuntimeBeanReference(matchingBen)); if (getMember() instanceof Field) { ((AutowireBeanReference) ref).setSource((Field) getMember()); } else { ((AutowireBeanReference) ref).setSource(getMember(), 0); } return Collections.singleton(ref); } } } } return Collections.emptySet(); } protected DependencyDescriptor[] getDependencyDescriptor(IAutowireDependencyResolver resolver) { if (getMember() instanceof Field) { return new DependencyDescriptor[] { new DependencyDescriptor((Field) getMember(), true) }; } else { Method method = (Method) this.member; Class<?>[] paramTypes = method.getParameterTypes(); DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { MethodParameter methodParam = new MethodParameter(method, i); descriptors[i] = new DependencyDescriptor(methodParam, true); } return descriptors; } } @Override public boolean shouldSkip(BeanDefinition bd) { return checkPropertySkipping(bd.getPropertyValues()); } } /** * Class representing injection information about an annotated field or setter method, supporting the @Resource * annotation. */ private class ResourceElement extends LookupElement { public ResourceElement(Member member, PropertyDescriptor pd, Class<? extends Annotation> annotationClass) { super(member, pd, annotationClass); } @Override protected void initAnnotation(AnnotatedElement ae, Class<? extends Annotation> annotationClass) { Annotation resource = ae.getAnnotation(annotationClass); String resourceName = (String) AnnotationUtils.getValue(resource, "name"); Class<?> resourceType = (Class<?>) AnnotationUtils.getValue(resource, "type"); this.isDefaultName = !StringUtils.hasLength(resourceName); if (this.isDefaultName) { resourceName = this.member.getName(); if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { resourceName = Introspector.decapitalize(resourceName.substring(3)); } } if (resourceType != null && !Object.class.equals(resourceType)) { checkResourceType(resourceType); } else { // No resource type specified... check field/method. resourceType = getResourceType(); } this.name = resourceName; this.lookupType = resourceType; this.mappedName = (String) AnnotationUtils.getValue(resource, "mappedName"); } @Override public Set<IBeanReference> getBeanReferences(IBean bean, IBeansModelElement context, IAutowireDependencyResolver resolver) { BeanDefinition bd = BeansModelUtils.getMergedBeanDefinition(bean, context); if (!shouldSkip(bd)) { if (StringUtils.hasLength(mappedName)) { String[] matchingBeans = resolver.getBeansForType(lookupType); for (String matchingBen : matchingBeans) { if (mappedName.equals(matchingBen)) { IBeanReference ref = new AutowireBeanReference(bean, new RuntimeBeanReference(matchingBen)); if (getMember() instanceof Field) { ((AutowireBeanReference) ref).setSource((Field) getMember()); } else { ((AutowireBeanReference) ref).setSource(getMember(), 0); } return Collections.singleton(ref); } } } else { return super.getBeanReferences(bean, context, resolver); } } return Collections.emptySet(); } } /** * Class representing injection information about an annotated field or setter method, supporting the @WebServiceRef * annotation. */ private class WebServiceRefElement extends LookupElement { public WebServiceRefElement(Member member, PropertyDescriptor pd, Class<? extends Annotation> annotationClass) { super(member, pd, annotationClass); } @Override protected void initAnnotation(AnnotatedElement ae, Class<? extends Annotation> annotationClass) { Annotation resource = ae.getAnnotation(annotationClass); String resourceName = (String) AnnotationUtils.getValue(resource, "name"); Class<?> resourceType = (Class<?>) AnnotationUtils.getValue(resource, "type"); this.isDefaultName = !StringUtils.hasLength(resourceName); if (this.isDefaultName) { resourceName = this.member.getName(); if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { resourceName = Introspector.decapitalize(resourceName.substring(3)); } } if (resourceType != null && !Object.class.equals(resourceType)) { checkResourceType(resourceType); } else { // No resource type specified... check field/method. resourceType = getResourceType(); } this.name = resourceName; if (Service.class.isAssignableFrom(resourceType)) { this.lookupType = resourceType; } else { this.lookupType = (!Object.class.equals(AnnotationUtils.getValue(resource, "value")) ? (Class<?>) AnnotationUtils .getValue(resource, "value") : Service.class); } this.mappedName = (String) AnnotationUtils.getValue(resource, "mappedName"); } } /** * Class representing injection information about an annotated field or setter method, supporting the @EJB * annotation. */ private class EjbRefElement extends LookupElement { private String beanName; public EjbRefElement(Member member, PropertyDescriptor pd, Class<? extends Annotation> annotationClass) { super(member, pd, annotationClass); } @Override protected void initAnnotation(AnnotatedElement ae, Class<? extends Annotation> annotationClass) { Annotation resource = ae.getAnnotation(annotationClass); String resourceBeanName = (String) AnnotationUtils.getValue(resource, "beanName"); String resourceName = (String) AnnotationUtils.getValue(resource, "name"); this.isDefaultName = !StringUtils.hasLength(resourceName); if (this.isDefaultName) { resourceName = this.member.getName(); if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { resourceName = Introspector.decapitalize(resourceName.substring(3)); } } Class<?> resourceType = (Class<?>) AnnotationUtils.getValue(resource, "beanInterface"); if (resourceType != null && !Object.class.equals(resourceType)) { checkResourceType(resourceType); } else { // No resource type specified... check field/method. resourceType = getResourceType(); } this.beanName = resourceBeanName; this.name = resourceName; this.lookupType = resourceType; this.mappedName = (String) AnnotationUtils.getValue(resource, "mappedName"); } @Override public Set<IBeanReference> getBeanReferences(IBean bean, IBeansModelElement context, IAutowireDependencyResolver resolver) { BeanDefinition bd = BeansModelUtils.getMergedBeanDefinition(bean, context); if (!shouldSkip(bd)) { if (StringUtils.hasLength(this.beanName)) { if (resolver.containsBean(this.beanName)) { String[] matchingBeans = resolver.getBeansForType(this.lookupType); for (String matchingBen : matchingBeans) { if (this.beanName.equals(matchingBen)) { IBeanReference ref = new AutowireBeanReference(bean, new RuntimeBeanReference( matchingBen)); if (getMember() instanceof Field) { ((AutowireBeanReference) ref).setSource((Field) getMember()); } else { ((AutowireBeanReference) ref).setSource(getMember(), 0); } return Collections.singleton(ref); } } } else if (this.isDefaultName && !StringUtils.hasLength(this.mappedName)) { // throw new NoSuchBeanDefinitionException(this.beanName, // "Cannot resolve 'beanName' in local BeanFactory. Consider specifying a general 'name' value instead."); } } } return super.getBeanReferences(bean, context, resolver); } } }