/******************************************************************************* * Copyright (c) 2009, 2012 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.ArrayList; import java.util.Arrays; import java.util.List; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.ide.eclipse.beans.core.model.validation.AbstractXmlValidationRule; import org.springframework.ide.eclipse.beans.core.model.validation.IXmlValidationContext; import org.springframework.ide.eclipse.beans.core.namespaces.NamespaceUtils; import org.springframework.ide.eclipse.beans.core.namespaces.ToolAnnotationUtils.ToolAnnotationData; import org.springframework.ide.eclipse.core.SpringCore; 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.model.validation.IValidationRule; import org.springframework.ide.eclipse.core.model.validation.ValidationProblemAttribute; import org.springframework.util.StringUtils; import org.springsource.ide.eclipse.commons.core.SpringCoreUtils; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * XML-based {@link IValidationRule} that uses Spring's Tool annotation to validate attribute values. * <p> * Also validates special XML namespace elements that don't support tool annotations. Currently the following attributes * and elements are supported: * <ul> * <li>util:constant static-field</li> * <li>osgi:interfaces value</li> * </ul> * @author Christian Dupuis * @author Terry Denney * @author Martin Lippert * @since 2.2.7 */ public class NamespaceElementsRule extends AbstractXmlValidationRule { private final List<IAttributeValidator> ATTRIBUTE_VALIDATORS; private final List<IElementValidator> ELEMENT_VALIDATORS; { ATTRIBUTE_VALIDATORS = new ArrayList<IAttributeValidator>(); ATTRIBUTE_VALIDATORS.add(new UtilStaticFieldAttributeValidator()); ATTRIBUTE_VALIDATORS.add(new BlueprintInterfaceAttributeValidator()); ATTRIBUTE_VALIDATORS.add(new BlueprintDependesOnAttributeValidator()); ELEMENT_VALIDATORS = new ArrayList<IElementValidator>(); ELEMENT_VALIDATORS.add(new OsgiInterfacesElementValidator()); } /** * Internal list of full-qualified class names that should be ignored by this validation rule. */ private List<String> ignorableClasses = null; /** * Internal list of bean names that should be ignored by this validation rule. */ private List<String> ignorableBeans = new ArrayList<String>(); public void setIgnorableClasses(String classNames) { if (StringUtils.hasText(classNames)) { this.ignorableClasses = Arrays.asList(StringUtils.delimitedListToStringArray(classNames, ",", "\r\n\f ")); } } public void setIgnorableBeans(String beanNames) { if (StringUtils.hasText(beanNames)) { this.ignorableBeans = Arrays.asList(StringUtils.delimitedListToStringArray(beanNames, ",", "\r\n\f ")); } } /** * {@inheritDoc} */ @Override protected boolean supports(Node n) { // only validate non standard nodes; nodes from the bean namespace are validated with all the subsequent // validation rules return n.getNodeType() == Node.ELEMENT_NODE && !NamespaceUtils.DEFAULT_NAMESPACE_URI.equals(n.getNamespaceURI()); } /** * {@inheritDoc} */ @Override protected void validate(Node n, IXmlValidationContext context) { validate(n, null, context); } public void validate(Node n, String attributeNameToCheck, IXmlValidationContext context) { if (attributeNameToCheck == null) { // Validate with one of the pre-configured validators for (IElementValidator validator : ELEMENT_VALIDATORS) { if (validator.supports(n)) { validator.validate(n, context); } } } // No Iterate over all the attributes to trigger validation of values NamedNodeMap attributes = n.getAttributes(); if (attributes != null && attributes.getLength() > 0) { for (int i = 0; i < attributes.getLength(); i++) { Node attribute = attributes.item(i); String attributeName = attribute.getLocalName(); // Only check attribute that is marked to check if (attributeNameToCheck != null && !attributeNameToCheck.equals(attributeName)) { continue; } // Attributes can be annotated with Tool annotation -> validate based on annotation for (ToolAnnotationData annotationData : context.getToolAnnotation(n, attributeName)) { // Check bean references if ("ref".equals(annotationData.getKind())) { validateBeanReference(n, attribute, context); } // Check class name if (Class.class.getName().equals(annotationData.getExpectedType())) { validateClassName(n, attribute, context, annotationData); } // Check method name if (annotationData.getExpectedMethodType() != null && attribute.getNodeValue() != null) { validateMethodName(evaluateXPathExpression(annotationData.getExpectedMethodType(), n), n, attribute, context); } else if (annotationData.getExpectedMethodRef() != null && attribute.getNodeValue() != null) { try { AbstractBeanDefinition referencedBeanDefinition = (AbstractBeanDefinition) context .getCompleteRegistry().getBeanDefinition( evaluateXPathExpression(annotationData.getExpectedMethodRef(), n)); String className = referencedBeanDefinition.getBeanClassName(); validateMethodName(className, n, attribute, context); } catch (NoSuchBeanDefinitionException e) { // Ignore this as it has already been reported } } } // Validate with one of the pre-configured validators for (IAttributeValidator validator : ATTRIBUTE_VALIDATORS) { if (validator.supports(n, attribute)) { validator.validate(n, attribute, context); } } } } } /** * Validate given method reference. */ private void validateMethodName(String className, Node n, Node attribute, IXmlValidationContext context) { IType type = JdtUtils.getJavaType(context.getRootElementProject(), className); try { if (type != null) { String methodName = attribute.getNodeValue(); if (Introspector.findMethod(type, methodName, -1, Public.DONT_CARE, Static.DONT_CARE) == null) { context.error(n, "METHOD_NOT_FOUND", "Method '" + methodName + "' not found in class '" + className + "'", new ValidationProblemAttribute("METHOD", methodName), new ValidationProblemAttribute("CLASS", className)); } } } catch (Exception e) { SpringCore.log(e); } } /** * Validate given class reference. */ private void validateClassName(Node n, Node attribute, IXmlValidationContext context, ToolAnnotationData annotationData) { String className = attribute.getNodeValue(); if (className != null && !SpringCoreUtils.hasPlaceHolder(className) && !ignorableClasses.contains(className)) { IType type = JdtUtils.getJavaType(context.getRootElementProject(), className); // Verify class is found if (type == null || (type.getDeclaringType() != null && className.indexOf('$') == -1)) { context.error(n, "CLASS_NOT_FOUND", "Class '" + className + "' not found", new ValidationProblemAttribute("CLASS", className)); return; } try { // Check if type is part for give type hierarchy if (annotationData.getAssignableTo() != null) { if (!JdtUtils.doesImplement(context.getRootElementResource(), type, annotationData .getAssignableTo())) { context.error(n, "CLASS_IS_NOT_IN_HIERACHY", "'" + className + "' is not a sub type of '" + annotationData.getAssignableTo() + "'"); } } // Check if type is either a concrete class or interface as requested by the tool // annotation if ("class-only".equals(annotationData.getAssignableToRestriction()) && type.isInterface()) { context.error(n, "CLASS_IS_INTERFACE", "'" + className + "' specifies an interface where a class is required"); } else if ("interface-only".equals(annotationData.getAssignableToRestriction()) && !type.isInterface()) { context.error(n, "CLASS_IS_CLASS", "'" + className + "' specifies a class where an interface is required"); } } catch (JavaModelException e) { } } } /** * Validate given bean reference. */ private void validateBeanReference(Node n, Node attribute, IXmlValidationContext context) { String beanName = attribute.getNodeValue(); if (beanName != null && !SpringCoreUtils.hasPlaceHolder(beanName) && !ignorableBeans.contains(beanName)) { try { context.getCompleteRegistry().getBeanDefinition(beanName); } catch (NoSuchBeanDefinitionException e) { context.warning(n, "UNDEFINED_REFERENCED_BEAN", "Referenced bean '" + beanName + "' not found", new ValidationProblemAttribute("BEAN", beanName)); } catch (BeanDefinitionStoreException e) { // Need to make sure that the parent of a parent does not use placeholders Throwable exp = e; boolean placeHolderFound = false; while (exp != null && exp.getCause() != null) { String msg = exp.getCause().getMessage(); if (msg.contains(SpringCoreUtils.PLACEHOLDER_PREFIX) && msg.contains(SpringCoreUtils.PLACEHOLDER_SUFFIX)) { placeHolderFound = true; break; } exp = exp.getCause(); } if (!placeHolderFound) { context.warning(n, "UNDEFINED_REFERENCED_BEAN", "Refrenced bean '" + beanName + "' not found", new ValidationProblemAttribute("BEAN", beanName)); } } } } /** * Evaluates XPath expressions against the given node. */ private String evaluateXPathExpression(String xpath, Node node) { XPathFactory factory = XPathFactory.newInstance(); XPath path = factory.newXPath(); try { return path.evaluate(xpath, node); } catch (XPathExpressionException e) { return null; } } /** * Implementations of this interface can validate attributes and their values. */ interface IAttributeValidator { boolean supports(Node n, Node attribute); void validate(Node n, Node attribute, IXmlValidationContext context); } /** * Implementations of this interface can validate elements. */ interface IElementValidator { boolean supports(Node n); void validate(Node n, IXmlValidationContext context); } /** * Simply validation for: * * <pre> * <util:constant static-field="org.springframework.core.Ordered.HIGHEST_PRECEDENCE"/> * </pre> */ private class UtilStaticFieldAttributeValidator implements IAttributeValidator { public boolean supports(Node n, Node attribute) { return "http://www.springframework.org/schema/util".equals(n.getNamespaceURI()) && "constant".equals(n.getLocalName()) && "static-field".equals(attribute.getNodeName()); } public void validate(Node n, Node attribute, IXmlValidationContext context) { try { String fieldName = attribute.getNodeValue(); if (fieldName != null) { int ix = fieldName.lastIndexOf('.'); if (ix > 0) { String className = fieldName.substring(0, ix); fieldName = fieldName.substring(ix + 1); IType type = JdtUtils.getJavaType(context.getRootElementProject(), className); if (type != null) { IField field = type.getField(fieldName); if (!field.exists()) { context.error(n, "FIELD_NOT_FOUND", "Field '" + fieldName + "' not found on class '" + className + "'", new ValidationProblemAttribute("CLASS", className), new ValidationProblemAttribute("FIELD", fieldName)); } else if (!type.isEnum() && !Flags.isStatic(field.getFlags())) { context.error(n, "FIELD_NOT_STATIC", "Field '" + fieldName + "' on class '" + className + "' is not static", new ValidationProblemAttribute("CLASS", className), new ValidationProblemAttribute("FIELD", fieldName)); } } else { context.error(n, "CLASS_NOT_FOUND", "Class '" + className + "' not found", new ValidationProblemAttribute("CLASS", className)); } } } } catch (JavaModelException e) { // Ignore here as we report class not found and such } } } /** * Simply validation for: * * <pre> * <osgi:reference id="test1" > * <osgi:interfaces> * <value>java.util.List</value> * </osgi:interfaces> * </osgi:reference> * </pre> */ private class OsgiInterfacesElementValidator implements IElementValidator { public boolean supports(Node n) { return ("http://www.springframework.org/schema/osgi".equals(n.getNamespaceURI()) || "http://www.osgi.org/xmlns/blueprint/v1.0.0" .equals(n.getNamespaceURI())) && "interfaces".equals(n.getLocalName()); } public void validate(Node n, IXmlValidationContext context) { NodeList children = n.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE && "value".equals(child.getLocalName()) && child.getFirstChild() != null && child.getFirstChild().getNodeType() == Node.TEXT_NODE) { String className = child.getFirstChild().getNodeValue(); IType type = JdtUtils.getJavaType(context.getRootElementProject(), className); // Verify class is found if (type == null || (type.getDeclaringType() != null && className.indexOf('$') == -1)) { context.error(child, "CLASS_NOT_FOUND", "Class '" + className + "' not found", new ValidationProblemAttribute("Class", className)); continue; } try { if (!type.isInterface()) { context.error(child, "CLASS_IS_CLASS", "'" + className + "' specifies a class where an interface is required"); } } catch (JavaModelException e) { } } } } } /** * Simply validation for: * * <pre> * <bp:reference interface="java.util.List" * </pre> */ private class BlueprintInterfaceAttributeValidator implements IAttributeValidator { private final ToolAnnotationData annotationData; public BlueprintInterfaceAttributeValidator() { this.annotationData = new ToolAnnotationData(); this.annotationData.setAssignableToRestriction("interface-only"); } public boolean supports(Node n, Node attribute) { return "http://www.osgi.org/xmlns/blueprint/v1.0.0".equals(n.getNamespaceURI()) && "interface".equals(attribute.getLocalName()); } public void validate(Node n, Node attribute, IXmlValidationContext context) { validateClassName(n, attribute, context, annotationData); } } /** * Simply validation for: * * <pre> * <bp:reference depends-on="ref" * </pre> */ private class BlueprintDependesOnAttributeValidator implements IAttributeValidator { public boolean supports(Node n, Node attribute) { return "http://www.osgi.org/xmlns/blueprint/v1.0.0".equals(n.getNamespaceURI()) && "depends-on".equals(attribute.getLocalName()); } public void validate(Node n, Node attribute, IXmlValidationContext context) { validateBeanReference(n, attribute, context); } } }