/*******************************************************************************
* Copyright (c) 2007, 2015 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.util.LinkedHashSet;
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.beans.PropertyAccessor;
import org.springframework.beans.factory.config.BeanDefinition;
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.IBeanProperty;
import org.springframework.ide.eclipse.beans.core.model.validation.AbstractNonInfrastructureBeanValidationRule;
import org.springframework.ide.eclipse.beans.core.model.validation.IBeansValidationContext;
import org.springframework.ide.eclipse.core.java.Introspector;
import org.springframework.ide.eclipse.core.java.Introspector.Public;
import org.springframework.ide.eclipse.core.java.Introspector.Static;
import org.springframework.ide.eclipse.core.java.JdtUtils;
import org.springframework.ide.eclipse.core.java.typehierarchy.TypeHierarchyEngine;
import org.springframework.ide.eclipse.core.model.IModelElement;
import org.springframework.ide.eclipse.core.model.validation.IValidationRule;
import org.springframework.ide.eclipse.core.model.validation.ValidationProblemAttribute;
import org.springframework.scripting.ScriptFactory;
import org.springframework.util.StringUtils;
import org.springsource.ide.eclipse.commons.core.SpringCoreUtils;
/**
* Validates a given {@link IBeanProperty}'s accessor methods in bean class.
* @author Torsten Juergeleit
* @author Christian Dupuis
* @author Terry Denney
* @author Martin Lippert
* @since 2.0
*/
public class BeanPropertyRule extends AbstractNonInfrastructureBeanValidationRule implements
IValidationRule<IBeanProperty, IBeansValidationContext> {
@Override
protected boolean supportsModelElementForNonInfrastructureBean(IModelElement element,
IBeansValidationContext context) {
return (element instanceof IBeanProperty
// Skip properties with placeholders
&& !SpringCoreUtils.hasPlaceHolder(((IBeanProperty) element).getElementName()));
}
public void validate(IBeanProperty property, IBeansValidationContext context, IProgressMonitor monitor) {
TypeHierarchyEngine typeEngine = getTypeHierarchyEngine(context);
IBean bean = (IBean) property.getElementParent();
BeanDefinition mergedBd = BeansModelUtils.getMergedBeanDefinition(bean, context.getContextElement());
String mergedClassName = mergedBd.getBeanClassName();
IType type = ValidationRuleUtils.extractBeanClass(mergedBd, bean, mergedClassName, context);
// Before checking for properties check that type is not a groovy type; groovy can dynamically add getters and
// setters
if (type != null && JdtUtils.isTypeGroovyElement(type)) {
return;
}
// Don't validate a bean without a valid and resolvable IType; this will
// be validated by the BeanClassRule
// Properties of abstract beans shouldn't be validated
// Don't validate property names on ScriptFactory implementations
// as these property values are injected into the created object rather
// then on the factory bean itself.
if (type != null && !mergedBd.isAbstract()
&& !JdtUtils.doesImplement(context.getRootElementResource(), type, ScriptFactory.class.getName(), typeEngine)) {
validateProperty(property, type, context, typeEngine);
}
}
private void validateProperty(IBeanProperty property, IType type, IBeansValidationContext context, TypeHierarchyEngine typeHierarchyEngine) {
String propertyName = property.getElementName();
// Check for property accessor in given type
try {
// First check for nested property path
int nestedIndex = getNestedPropertySeparatorIndex(propertyName, false);
String className = type.getFullyQualifiedName();
if (nestedIndex >= 0) {
String nestedPropertyName = propertyName.substring(0, nestedIndex);
PropertyTokenHolder tokens = getPropertyNameTokens(nestedPropertyName);
String getterName = "get" + StringUtils.capitalize(tokens.actualName);
IMethod getter = Introspector.findMethod(type, getterName, 0, Public.YES, Static.NO, typeHierarchyEngine);
if (getter == null) {
context.error(property, "NO_GETTER", "No getter found for nested property '" + nestedPropertyName
+ "' in class '" + className + "'", new ValidationProblemAttribute("CLASS", className),
new ValidationProblemAttribute("PROPERTY", nestedPropertyName),
new ValidationProblemAttribute("BEAN_NAME", ValidationRuleUtils.getBeanName(property)));
}
else {
// Check getter's return type
if (tokens.keys != null) {
// TODO Check getter's return type for index or map
// type
}
}
}
else {
// Now check for mapped property
int mappedIndex = propertyName.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
if (mappedIndex != -1) {
propertyName = propertyName.substring(0, mappedIndex);
}
// Finally check property
if (!Introspector.isValidPropertyName(propertyName)) {
context.error(property, "INVALID_PROPERTY_NAME", "Invalid property name '" + propertyName
+ "' - not JavaBean compliant", new ValidationProblemAttribute("CLASS", className),
new ValidationProblemAttribute("PROPERTY", propertyName),
new ValidationProblemAttribute("BEAN_NAME", ValidationRuleUtils.getBeanName(property)));
}
else if (!Introspector.hasWritableProperty(type, propertyName, typeHierarchyEngine)) {
context.error(property, "NO_SETTER", "No setter found for property '" + propertyName
+ "' in class '" + className + "'", new ValidationProblemAttribute("CLASS", className),
new ValidationProblemAttribute("PROPERTY", propertyName),
new ValidationProblemAttribute("BEAN_NAME", ValidationRuleUtils.getBeanName(property)));
}
// TODO If mapped property then check type of setter's argument
}
}
catch (JavaModelException e) {
BeansCorePlugin.log(e);
}
}
/**
* Determine the first (or last) nested property separator in the given property path, ignoring dots in keys (like
* "map[my.key]").
* @param propertyPath the property path to check
* @param last whether to return the last separator rather than the first
* @return the index of the nested property separator, or -1 if none
*/
private int getNestedPropertySeparatorIndex(String propertyPath, boolean last) {
boolean inKey = false;
int i = (last ? propertyPath.length() - 1 : 0);
while ((last && i >= 0) || i < propertyPath.length()) {
switch (propertyPath.charAt(i)) {
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR:
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR:
inKey = !inKey;
break;
case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR:
if (!inKey) {
return i;
}
}
if (last) {
i--;
}
else {
i++;
}
}
return -1;
}
/**
* Parse the given property name into the corresponding property name tokens.
*
* @param propertyName the property name to parse
* @return representation of the parsed property tokens
*/
private PropertyTokenHolder getPropertyNameTokens(String propertyName) {
PropertyTokenHolder tokens = new PropertyTokenHolder();
String actualName = null;
Set<String> keys = new LinkedHashSet<String>(2);
int searchIndex = 0;
while (searchIndex != -1) {
int keyStart = propertyName.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX, searchIndex);
searchIndex = -1;
if (keyStart != -1) {
int keyEnd = propertyName.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart
+ PropertyAccessor.PROPERTY_KEY_PREFIX.length());
if (keyEnd != -1) {
if (actualName == null) {
actualName = propertyName.substring(0, keyStart);
}
String key = propertyName.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(),
keyEnd);
if (key.startsWith("'") && key.endsWith("'")) {
key = key.substring(1, key.length() - 1);
}
else if (key.startsWith("\"") && key.endsWith("\"")) {
key = key.substring(1, key.length() - 1);
}
keys.add(key);
searchIndex = keyEnd + PropertyAccessor.PROPERTY_KEY_SUFFIX.length();
}
}
}
tokens.actualName = (actualName != null ? actualName : propertyName);
tokens.canonicalName = tokens.actualName;
if (!keys.isEmpty()) {
tokens.canonicalName += PropertyAccessor.PROPERTY_KEY_PREFIX
+ StringUtils.collectionToDelimitedString(keys, PropertyAccessor.PROPERTY_KEY_SUFFIX
+ PropertyAccessor.PROPERTY_KEY_PREFIX) + PropertyAccessor.PROPERTY_KEY_SUFFIX;
tokens.keys = keys.toArray(new String[keys.size()]);
}
return tokens;
}
private static class PropertyTokenHolder {
private String canonicalName;
private String actualName;
private String[] keys;
}
}