/******************************************************************************* * Copyright (c) 2007, 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.internal.model.validation.rules; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.springframework.asm.AnnotationVisitor; import org.springframework.asm.ClassReader; import org.springframework.asm.ClassVisitor; import org.springframework.asm.MethodVisitor; import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.annotation.Required; import org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.ide.eclipse.beans.core.BeansCorePlugin; 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.validation.AbstractBeanValidationRule; import org.springframework.ide.eclipse.beans.core.model.validation.IBeansValidationContext; import org.springframework.ide.eclipse.core.java.Introspector; import org.springframework.ide.eclipse.core.model.validation.ValidationProblemAttribute; import org.springframework.ide.eclipse.core.type.asm.AnnotationMetadataReadingVisitor; import org.springframework.ide.eclipse.core.type.asm.ClassReaderFactory; import org.springframework.ide.eclipse.core.type.asm.EmptyAnnotationVisitor; import org.springframework.ide.eclipse.core.type.asm.EmptyMethodVisitor; /** * Validates a given {@link IBean}'s if all {@link Required} annotated properties are configured. * * @author Christian Dupuis * @author Terry Denney * @author Martin Lippert * @since 2.0.1 */ public class RequiredPropertyRule extends AbstractBeanValidationRule { private static final String REQUIRED_ANNOTATION_TYPE_PROPERTY_NAME = "requiredAnnotationType"; @Override protected boolean supportsBean(IBean bean, IBeansValidationContext context) { return !bean.isAbstract() && context.isBeanRegistered(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME, RequiredAnnotationBeanPostProcessor.class.getName()); } /** * Validates the given {@link IBean}. * <p> * First checks if the bean is not abstract and if the {@link RequiredAnnotationBeanPostProcessor} is registered in * the application context. If so the bean class is scanned by using an ASM-based {@link ClassVisitor} for any * {@link Required} annotated property setters. */ @Override public void validate(IBean bean, IBeansValidationContext context, IProgressMonitor monitor) { BeanDefinition mergedBd = BeansModelUtils.getMergedBeanDefinition(bean, context.getContextElement()); String mergedClassName = mergedBd.getBeanClassName(); IType type = ValidationRuleUtils.extractBeanClass(mergedBd, bean, mergedClassName, context); if (type != null) { validatePropertyNames(type, bean, mergedBd, context); } } /** * Validates {@link PropertyValues} of given {link BeanDefinition} if all required are configured. * @param type the type whose hierarchy to check for {@link Required} annotated properties * @param bean the underlying {@link IBean} instance * @param mergedBd the {@link BeanDefinition} behind the {@link IBean} * @param context context to retrieve a {@link ClassReaderFactory} and report errors */ private void validatePropertyNames(IType type, IBean bean, BeanDefinition mergedBd, IBeansValidationContext context) { try { RequiredAnnotationMetadata annotationMetadata = getRequiredAnnotationMetadata(context .getClassReaderFactory(), bean, type, getRequiredAnnotationTypes(context)); List<String> missingProperties = new ArrayList<String>(); Set<IMethod> properties = Introspector.findAllWritableProperties(type); for (IMethod property : properties) { String propertyName = java.beans.Introspector.decapitalize(property.getElementName().substring(3)); if (annotationMetadata.isRequiredProperty(propertyName) && mergedBd.getPropertyValues().getPropertyValue(propertyName) == null) { missingProperties.add(propertyName); } } // add the error message if (missingProperties.size() > 0) { String msg = buildExceptionMessage(missingProperties, bean.getElementName()); context.error(bean, "REQUIRED_PROPERTY_MISSING", msg, new ValidationProblemAttribute("CLASS", type .getFullyQualifiedName()), new ValidationProblemAttribute("BEAN_NAME", bean.getElementName()), new ValidationProblemAttribute("MISSING_PROPERTIES", missingProperties)); } } catch (JavaModelException e) { BeansCorePlugin.log(e); } } /** * Retrieves a instance of {@link RequiredAnnotationMetadata} that contains information about used annotations in * the class under question */ private RequiredAnnotationMetadata getRequiredAnnotationMetadata(final ClassReaderFactory classReaderFactory, final IBean bean, final IType type, Set<String> requiredAnnotationTypes) { String className = type.getFullyQualifiedName(); RequiredAnnotationMetadata visitor = new RequiredAnnotationMetadata(requiredAnnotationTypes); try { while (className != null && !Object.class.getName().equals(className)) { ClassReader classReader = classReaderFactory.getClassReader(className); classReader.accept(visitor, 0); className = visitor.getSuperClassName(); } } catch (IOException e) { // ignore any missing files here as this will be // reported as missing bean class } return visitor; } /** * ASM based visitor that checks the precedence of an {@link Required} annotation on <b>any</b> property setter. */ private static class RequiredAnnotationMetadata extends AnnotationMetadataReadingVisitor { private Set<String> requiredAnnotationTypes = new HashSet<String>(); private Set<String> requiredPropertyNames = new HashSet<String>(); public RequiredAnnotationMetadata(Set<String> requiredAnnotationTypes) { for (String className : requiredAnnotationTypes) { this.requiredAnnotationTypes.add('L' + className.replace('.', '/') + ';'); } } @Override public MethodVisitor visitMethod(int modifier, final String name, String params, String arg3, String[] arg4) { if (name.startsWith("set")) { return new EmptyMethodVisitor() { @Override public AnnotationVisitor visitAnnotation(final String desc, boolean visible) { if (requiredAnnotationTypes.contains(desc)) { requiredPropertyNames.add(java.beans.Introspector.decapitalize(name.substring(3))); } return new EmptyAnnotationVisitor(); } }; } return new EmptyMethodVisitor(); } public boolean isRequiredProperty(String propertyName) { return requiredPropertyNames.contains(propertyName); } } /** * Extracts the configured <code>requiredAnnotationType</code> values from all registered * {@link RequiredAnnotationBeanPostProcessor}. * @since 2.0.2 */ private Set<String> getRequiredAnnotationTypes(IBeansValidationContext context) { Set<String> requiredAnnotationTypes = new HashSet<String>(); Set<BeanDefinition> bds = context.getRegisteredBeanDefinition( AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME, RequiredAnnotationBeanPostProcessor.class.getName()); for (BeanDefinition bd : bds) { PropertyValue property = bd.getPropertyValues().getPropertyValue(REQUIRED_ANNOTATION_TYPE_PROPERTY_NAME); if (property != null && property.getValue() instanceof TypedStringValue) { requiredAnnotationTypes.add(((TypedStringValue) property.getValue()).getValue()); } else { requiredAnnotationTypes.add(Required.class.getName()); } } return requiredAnnotationTypes; } /** * Build an exception message for the given list of invalid properties. * @param invalidProperties the list of names of invalid properties * @param beanName the name of the bean * @return the exception message */ private String buildExceptionMessage(List<String> invalidProperties, String beanName) { int size = invalidProperties.size(); StringBuilder sb = new StringBuilder(); sb.append(size == 1 ? "Property" : "Properties"); for (int i = 0; i < size; i++) { String propertyName = invalidProperties.get(i); if (i > 0) { if (i == (size - 1)) { sb.append(" and"); } else { sb.append(","); } } sb.append(" '").append(propertyName).append("'"); } sb.append(size == 1 ? " is" : " are"); sb.append(" required for bean '").append(beanName).append("'"); return sb.toString(); } }