/*
* JBoss, Home of Professional Open Source.
* Copyright 2010, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.deployment.managedbean.processors;
import org.jboss.as.deployment.DeploymentPhases;
import org.jboss.as.deployment.managedbean.config.InterceptorConfiguration;
import org.jboss.as.deployment.managedbean.config.ManagedBeanConfiguration;
import org.jboss.as.deployment.managedbean.config.ManagedBeanConfigurations;
import org.jboss.as.deployment.managedbean.config.ResourceConfiguration;
import org.jboss.as.deployment.module.ModuleDeploymentProcessor;
import org.jboss.as.deployment.processor.AnnotationIndexProcessor;
import org.jboss.as.deployment.unit.DeploymentUnitContext;
import org.jboss.as.deployment.unit.DeploymentUnitProcessingException;
import org.jboss.as.deployment.unit.DeploymentUnitProcessor;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.Index;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.modules.Module;
import javax.annotation.ManagedBean;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.annotation.Resources;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptors;
import javax.interceptor.InvocationContext;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Deployment unit processor responsible for scanning a deployment to find classes with {@code javax.annotation.ManagedBean} annotations.
* Note: This processor only supports JSR-316 compliant managed beans. So it will not handle complimentary spec additions (ex. EJB).
*
* @author John E. Bailey
*/
public class ManagedBeanAnnotationProcessor implements DeploymentUnitProcessor {
public static final long PRIORITY = DeploymentPhases.POST_MODULE_DESCRIPTORS.plus(100L);
private static final DotName MANAGED_BEAN_ANNOTATION_NAME = DotName.createSimple(ManagedBean.class.getName());
private static final DotName RESOURCE_ANNOTATION_NAME = DotName.createSimple(Resource.class.getName());
private static final DotName INTERCEPTORS_ANNOTATION_NAME = DotName.createSimple(Interceptors.class.getName());
/**
* Check the deployment annotation index for all classes with the @ManagedBean annotation. For each class with the
* annotation, collect all the required information to create a managed bean instance, and attach it to the context.
*
* @param context the deployment unit context
* @throws DeploymentUnitProcessingException
*/
public void processDeployment(DeploymentUnitContext context) throws DeploymentUnitProcessingException {
if (context.getAttachment(ManagedBeanConfigurations.ATTACHMENT_KEY) != null) {
return; // Skip if the configurations already exist
}
final Index index = context.getAttachment(AnnotationIndexProcessor.ATTACHMENT_KEY);
if (index == null) {
return; // Skip if there is no annotation index
}
final List<AnnotationTarget> targets = index.getAnnotationTargets(MANAGED_BEAN_ANNOTATION_NAME);
if (targets == null) {
return; // Skip if there are no ManagedBean instances
}
final Module module = context.getAttachment(ModuleDeploymentProcessor.MODULE_ATTACHMENT_KEY);
if (module == null)
throw new DeploymentUnitProcessingException("Manged bean annotation processing requires a module.");
final ClassLoader classLoader = module.getClassLoader();
final ManagedBeanConfigurations managedBeanConfigurations = new ManagedBeanConfigurations();
context.putAttachment(ManagedBeanConfigurations.ATTACHMENT_KEY, managedBeanConfigurations);
for (AnnotationTarget target : targets) {
if (!(target instanceof ClassInfo)) {
throw new DeploymentUnitProcessingException("The ManagedBean annotation is only allowed at the class level: " + target);
}
final ClassInfo classInfo = ClassInfo.class.cast(target);
final String beanClassName = classInfo.name().toString();
final Class<?> beanClass;
try {
beanClass = classLoader.loadClass(beanClassName);
} catch (ClassNotFoundException e) {
throw new DeploymentUnitProcessingException("Failed to load managed bean class: " + beanClassName);
}
// Get the managed bean name from the annotation
final ManagedBean managedBeanAnnotation = beanClass.getAnnotation(ManagedBean.class);
final String beanName = managedBeanAnnotation.value().isEmpty() ? beanClassName : managedBeanAnnotation.value();
final ManagedBeanConfiguration managedBeanConfiguration = new ManagedBeanConfiguration(beanName, beanClass);
processLifecycleMethods(managedBeanConfiguration, beanClass, index);
final Map<DotName, List<AnnotationTarget>> classAnnotations = classInfo.annotations();
managedBeanConfiguration.setResourceConfigurations(processResources(classAnnotations, beanClass));
managedBeanConfiguration.setInterceptorConfigurations(processInterceptors(index, classAnnotations, beanClass));
managedBeanConfigurations.add(managedBeanConfiguration);
}
}
private void processLifecycleMethods(final ManagedBeanConfiguration managedBeanConfiguration, final Class<?> beanClass, final Index index) throws DeploymentUnitProcessingException {
final List<Method> postConstructMethods = new ArrayList<Method>();
final List<Method> preDestroyMethods = new ArrayList<Method>();
Class<?> current = beanClass;
while(current != null && !Object.class.equals(current)) {
final ClassInfo classInfo = index.getClassByName(DotName.createSimple(current.getName()));
final Method postConstructMethod = getSingleAnnotatedMethod(current, classInfo, PostConstruct.class, false);
if(postConstructMethod != null) {
postConstructMethods.add(postConstructMethod);
}
final Method preDestroyMethod = getSingleAnnotatedMethod(current, classInfo, PreDestroy.class, false);
if(preDestroyMethod != null) {
preDestroyMethods.add(preDestroyMethod);
}
current = current.getSuperclass();
}
managedBeanConfiguration.setPostConstructMethods(postConstructMethods);
managedBeanConfiguration.setPreDestroyMethods(preDestroyMethods);
}
private List<InterceptorConfiguration> processInterceptors(final Index index, final Map<DotName, List<AnnotationTarget>> beanClassAnnotations, final Class<?> beanClass) throws DeploymentUnitProcessingException {
final List<AnnotationTarget> interceptorTargets = beanClassAnnotations.get(INTERCEPTORS_ANNOTATION_NAME);
if (interceptorTargets == null || interceptorTargets.isEmpty()) {
return Collections.emptyList();
}
final List<InterceptorConfiguration> interceptorConfigurations = new ArrayList<InterceptorConfiguration>(interceptorTargets.size());
final Interceptors interceptorsAnnotation = beanClass.getAnnotation(Interceptors.class);
final Class<?>[] interceptorTypes = interceptorsAnnotation.value();
for(Class<?> interceptorType : interceptorTypes) {
final ClassInfo classInfo = index.getClassByName(DotName.createSimple(interceptorType.getName()));
if(classInfo == null)
continue; // TODO: Process without index info
final Map<DotName, List<AnnotationTarget>> interceptorClassAnnotations = classInfo.annotations();
final Method aroundInvokeMethod = getSingleAnnotatedMethod(interceptorType, classInfo, AroundInvoke.class, true);
final List<ResourceConfiguration> resourceConfigurations = processResources(interceptorClassAnnotations, interceptorType);
interceptorConfigurations.add(new InterceptorConfiguration(interceptorType, aroundInvokeMethod, resourceConfigurations));
}
//Look for any @AroundInvoke methods on bean class
final ClassInfo classInfo = index.getClassByName(DotName.createSimple(beanClass.getName()));
if (classInfo != null) {
final Method aroundInvokeMethod = getSingleAnnotatedMethod(beanClass, classInfo, AroundInvoke.class, true);
if (aroundInvokeMethod != null) {
final List<ResourceConfiguration> resources = processClassResources(beanClass);
interceptorConfigurations.add(new InterceptorConfiguration(beanClass, aroundInvokeMethod, resources));
}
}
return interceptorConfigurations;
}
private Method getSingleAnnotatedMethod(final Class<?> type, final ClassInfo classInfo, final Class<? extends Annotation> annotationType, final boolean requireInvocationContext) throws DeploymentUnitProcessingException {
Method method = null;
if(classInfo != null) {
// Try to resolve with the help of the annotation index
final Map<DotName, List<AnnotationTarget>> classAnnotations = classInfo.annotations();
final List<AnnotationTarget> targets = classAnnotations.get(DotName.createSimple(annotationType.getName()));
if (targets == null || targets.isEmpty()) {
return null;
}
if (targets.size() > 1) {
throw new DeploymentUnitProcessingException("Only one method may be annotated with " + annotationType + " per managed bean.");
}
final AnnotationTarget target = targets.get(0);
if (!(target instanceof MethodInfo)) {
throw new DeploymentUnitProcessingException(annotationType + " is only valid on method targets.");
}
final MethodInfo methodInfo = MethodInfo.class.cast(target);
final Type[] args = methodInfo.args();
try {
switch (args.length) {
case 0:
if(requireInvocationContext) {
throw new DeploymentUnitProcessingException("Missing argument. Methods annotated with " + annotationType + " must have either single InvocationContext argument.");
}
method = type.getDeclaredMethod(methodInfo.name());
break;
case 1:
if(!InvocationContext.class.getName().equals(args[0].name().toString())) {
throw new DeploymentUnitProcessingException("Invalid argument type. Methods annotated with " + annotationType + " must have either single InvocationContext argument.");
}
method = type.getDeclaredMethod(methodInfo.name(), InvocationContext.class);
break;
default:
throw new DeploymentUnitProcessingException("Invalid number of arguments for method " + methodInfo.name() + " annotated with " + annotationType + " on class " + type.getName());
}
} catch(NoSuchMethodException e) {
throw new DeploymentUnitProcessingException("Failed to get " + annotationType + " method for type: " + type.getName(), e);
}
} else {
// No index information. Default to normal reflection
for(Method typeMethod : type.getDeclaredMethods()) {
if(method.isAnnotationPresent(annotationType)) {
method = typeMethod;
switch (method.getParameterTypes().length){
case 0:
if(requireInvocationContext) {
throw new DeploymentUnitProcessingException("Method " + method.getName() + " annotated with " + annotationType + " must have a single InvocationContext parameter");
}
break;
case 1:
if(!InvocationContext.class.equals(method.getParameterTypes()[0])) {
throw new DeploymentUnitProcessingException("Method " + method.getName() + " annotated with " + annotationType + " must have a single InvocationContext parameter.");
}
default:
throw new DeploymentUnitProcessingException("Methods " + method.getName() + " annotated with " + annotationType + " can only have a single InvocationContext parameter.");
}
break;
}
}
}
if(method != null) {
method.setAccessible(true);
}
return method;
}
private List<ResourceConfiguration> processResources(final Map<DotName, List<AnnotationTarget>> classAnnotations, final Class<?> owningClass) throws DeploymentUnitProcessingException {
final List<AnnotationTarget> resourceTargets = classAnnotations.get(RESOURCE_ANNOTATION_NAME);
if (resourceTargets == null) {
return Collections.emptyList();
}
final List<ResourceConfiguration> resourceConfigurations = new ArrayList<ResourceConfiguration>(resourceTargets.size());
for (AnnotationTarget annotationTarget : resourceTargets) {
final ResourceConfiguration resourceConfiguration;
if (annotationTarget instanceof FieldInfo) {
resourceConfiguration = processFieldResource(FieldInfo.class.cast(annotationTarget), owningClass);
} else if(annotationTarget instanceof MethodInfo) {
resourceConfiguration = processMethodResource(MethodInfo.class.cast(annotationTarget), owningClass);
} else if(annotationTarget instanceof ClassInfo) {
final Resource resource = owningClass.getAnnotation(Resource.class);
if(resource == null) {
throw new DeploymentUnitProcessingException("Failed to get @Resource annotation from class " + owningClass.getName());
}
resourceConfiguration = processClassResource(owningClass, resource);
} else {
continue;
}
if (resourceConfiguration != null) {
resourceConfigurations.add(resourceConfiguration);
}
}
resourceConfigurations.addAll(processClassResources(owningClass));
return resourceConfigurations;
}
private ResourceConfiguration processFieldResource(final FieldInfo fieldInfo, final Class<?> owningClass) throws DeploymentUnitProcessingException {
final String fieldName = fieldInfo.name();
final Field field;
try {
field = owningClass.getDeclaredField(fieldName);
field.setAccessible(true);
} catch(NoSuchFieldException e) {
throw new DeploymentUnitProcessingException("Failed to get field '" + fieldName + "' from class '" + owningClass + "'", e);
}
final Resource resource = field.getAnnotation(Resource.class);
if (resource != null) {
final String localContextName = resource.name().isEmpty() ? fieldName : resource.name();
final Class<?> injectionType = resource.type().equals(Object.class) ? field.getType() : resource.type();
return new ResourceConfiguration(fieldName, field, ResourceConfiguration.TargetType.FIELD, injectionType, localContextName, getTargetContextName(resource, fieldName, injectionType));
}
return null;
}
private ResourceConfiguration processMethodResource(final MethodInfo methodInfo, final Class<?> owningClass) throws DeploymentUnitProcessingException {
final String methodName = methodInfo.name();
if (!methodName.startsWith("set") || methodInfo.args().length != 1) {
throw new DeploymentUnitProcessingException("@Resource injection target is invalid. Only setter methods are allowed: " + methodInfo);
}
final Method method;
try {
method = owningClass.getMethod(methodName);
method.setAccessible(true);
} catch (NoSuchMethodException e) {
throw new DeploymentUnitProcessingException("Failed to get method '" + methodName + "' from class '" + owningClass + "'", e);
}
final Resource resource = method.getAnnotation(Resource.class);
if (resource != null) {
final String contextNameSuffix = methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
final Class<?> injectionType = resource.type().equals(Object.class) ? method.getReturnType() : resource.type();
final String localContextName = resource.name().isEmpty() ? contextNameSuffix : resource.name();
return new ResourceConfiguration(methodName, method, ResourceConfiguration.TargetType.METHOD, injectionType, localContextName, getTargetContextName(resource, contextNameSuffix, injectionType));
}
return null;
}
private ResourceConfiguration processClassResource(final Class<?> owningClass, final Resource resource) throws DeploymentUnitProcessingException {
if(resource.name().isEmpty()) {
throw new DeploymentUnitProcessingException("Class level @Resource annotations must provide a name.");
}
if(resource.mappedName().isEmpty()) {
throw new DeploymentUnitProcessingException("Class level @Resource annotations must provide a mapped name.");
}
if(Object.class.equals(resource.type())) {
throw new DeploymentUnitProcessingException("Class level @Resource annotations must provide a type.");
}
return new ResourceConfiguration(owningClass.getName(), null, ResourceConfiguration.TargetType.CLASS, resource.type(), resource.name(), resource.mappedName());
}
private List<ResourceConfiguration> processClassResources(final Class<?> owningClass) throws DeploymentUnitProcessingException {
final Resources resources = owningClass.getAnnotation(Resources.class);
if(resources == null) {
return Collections.emptyList();
}
final Resource[] resourceAnnotations = resources.value();
final List<ResourceConfiguration> resourceConfigurations = new ArrayList<ResourceConfiguration>(resourceAnnotations.length);
for(Resource resource : resourceAnnotations) {
resourceConfigurations.add(processClassResource(owningClass, resource));
}
return resourceConfigurations;
}
private String getTargetContextName(final Resource resource, final String contextNameSuffix, final Class<?> injectionType) throws DeploymentUnitProcessingException {
String targetContextName = resource.mappedName(); // TODO: Figure out how to use .lookup in IDE/Maven
if(targetContextName.isEmpty()) {
if(isEnvironmentEntryType(injectionType)) {
targetContextName = contextNameSuffix;
} else if(injectionType.isAnnotationPresent(ManagedBean.class)) {
final ManagedBean managedBean = injectionType.getAnnotation(ManagedBean.class);
targetContextName = managedBean.value().isEmpty() ? injectionType.getName() : managedBean.value();
} else {
throw new DeploymentUnitProcessingException("Unable to determine mapped name for @Resource injection.");
}
}
return targetContextName;
}
private boolean isEnvironmentEntryType(Class<?> type) {
return type.equals(String.class)
|| type.equals(Character.class)
|| type.equals(Byte.class)
|| type.equals(Short.class)
|| type.equals(Integer.class)
|| type.equals(Long.class)
|| type.equals(Boolean.class)
|| type.equals(Double.class)
|| type.equals(Float.class)
|| type.isPrimitive();
}
}