/******************************************************************************* * Copyright (c) 2008, 2009 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; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.wst.sse.core.StructuredModelManager; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration; import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery; import org.eclipse.wst.xml.core.internal.document.DOMModelImpl; import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil; import org.eclipse.wst.xsd.contentmodel.internal.XSDImpl.XSDElementDeclarationAdapter; import org.eclipse.xsd.impl.XSDElementDeclarationImpl; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.ComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.xml.NamespaceHandler; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.ide.eclipse.beans.core.BeansCorePlugin; import org.springframework.ide.eclipse.beans.core.model.IBeansConfig; import org.springframework.ide.eclipse.core.SpringCorePreferences; import org.springframework.util.StringUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * {@link NamespaceHandler} that creates {@link BeanDefinition}s based on Spring's tool namespace. * <p> * The following annotation is currently supported: * <pre> * <xsd:element name="foobar"> * <xsd:annotation> * <xsd:appinfo> * <tool:annotation> * <tool:exports type="com.foo.Bar"/> * </tool:annotation> * </xsd:appinfo> * </xsd:annotation> * ... * </xsd:element> * </pre> * Using the above XSD definition in the following form * <pre> * <foobar id="foobeanchen" /> * </pre> * would create a BeanDefinition with bean class <code>com.foo.Bar</code> and id * <code>foobeanchen</code>. * @author Christian Dupuis * @since 2.0.3 */ @SuppressWarnings("restriction") public class ToolAnnotationBasedNamespaceHandler implements NamespaceHandler { private static final String DEFAULT_ID_XPATH = "@id"; private static final String TYPE_ATTRIBUTE = "type"; private static final String IDENTIFIER_ATTRIBUTE = "identifier"; private static final String EXPORTS_ELEMENT = "exports"; private static final String ANNOTATION_ELEMENT = "annotation"; private static final String TOOL_NAMESPACE_URI = "http://www.springframework.org/schema/tool"; private final IBeansConfig beansConfig; /** * Constructor taking a {@link IBeansConfig}. */ public ToolAnnotationBasedNamespaceHandler(IBeansConfig file) { this.beansConfig = file; } public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext) { // Don't do anything; there are no decoration annotations defined. return definition; } public void init() { // Don't do anything. } /** * Entry point for the parsing a given element. * <p> * This implementation simply delegates to {@link #parseElement(Node, ParserContext)} and * registers the returned {@link ComponentDefinition} with the given {@link ParserContext}. * <p> */ public BeanDefinition parse(Element element, ParserContext parserContext) { // Get a component definition for the given element. ComponentDefinition componentDefinition = parseElement(element, parserContext); if (componentDefinition != null) { if (componentDefinition instanceof BeanComponentDefinition) { parserContext.registerBeanComponent((BeanComponentDefinition) componentDefinition); // return the bean definition for the use within Spring return ((BeanComponentDefinition) componentDefinition).getBeanDefinition(); } parserContext.registerComponent(componentDefinition); } else { // emit a warning that the NamespaceHandler cannot be found, only if the property is not // set by the user if (!SpringCorePreferences.getProjectPreferences( beansConfig.getElementResource().getProject(), BeansCorePlugin.PLUGIN_ID) .getBoolean(BeansCorePlugin.IGNORE_MISSING_NAMESPACEHANDLER_PROPERTY, false)) { parserContext.getReaderContext().warning( "Unable to locate Spring NamespaceHandler for element '" + element.getNodeName() + "' of schema namespace '" + element.getNamespaceURI() + "'", parserContext.extractSource(element)); } } return null; } /** * Parses the given element and looks for a tool:annotation -> tool:export definition on the * corresponding XSD definition. * <p> * First tries to get a {@link ComponentDefinition} from the element itself. After that the * child nodes of the given elements are checked for tool meta data annotations. * <p> * If there are annotations on child elements a {@link CompositeComponentDefinition} will be * created carrying the nested {@link ComponentDefinition}s. * @param element the element to parse * @param parserContext the current parser context * @return a {@link ComponentDefinition} created from the given element */ private ComponentDefinition parseElement(Node element, ParserContext parserContext) { // Get - if any - the component for the given element. ComponentDefinition rootComponent = parseSingleElement(element, parserContext); List<ComponentDefinition> nestedComponents = new ArrayList<ComponentDefinition>(); NodeList nestedElements = element.getChildNodes(); for (int i = 0; i < nestedElements.getLength(); i++) { Node nestedElement = nestedElements.item(i); if (nestedElement.getNodeType() == Node.ELEMENT_NODE) { ComponentDefinition nestedComponent = parseElement(nestedElement, parserContext); if (nestedComponent != null) { nestedComponents.add(nestedComponent); } } } if (nestedComponents.size() > 0) { CompositeComponentDefinition compositeComponentDefinition = new CompositeComponentDefinition( element.getNodeName(), parserContext.extractSource(element)); for (ComponentDefinition componentDefinition : nestedComponents) { compositeComponentDefinition.addNestedComponent(componentDefinition); } // TODO CD implement custom CompositeComponentDefinition to carry // along the rootComponent if any. return compositeComponentDefinition; } return rootComponent; } /** * Parses a single element and looks for tool:annotations on the corresponding XSD definition. * <p> * The tool:annotation can either be on the type definition itself or on a referenced type * definition. If a annotation is found the type itself it will overrule any other on the * referenced type. * @param element the element to parse * @param parserContext the current parser context * @return a {@link ComponentDefinition} created from the given element */ private ComponentDefinition parseSingleElement(Node element, ParserContext parserContext) { IStructuredModel model = null; try { // Load the StructedModel for the given file. model = getStructuredModel(); if (model != null) { String localName = element.getLocalName(); String namespaceUri = element.getNamespaceURI(); // Get a list of nodes for given localName and namespace from // the loaded StructuredModel Document document = ((DOMModelImpl) model).getDocument(); NodeList nodes = document.getElementsByTagNameNS(namespaceUri, localName); if (nodes.getLength() > 0) { // Only interested in one node as we need to get the // attached meta data and not the concrete element data Node node = nodes.item(0); // Look for meta data. The query returns null in case no XSD // can be found. ModelQuery modelQuery = ModelQueryUtil.getModelQuery(document); if (modelQuery != null) { // Get the element declaration from the model query. CMElementDeclaration result = modelQuery .getCMElementDeclaration((Element) node); // If the XSD is not found this check will fail. if (result instanceof XSDElementDeclarationAdapter) { XSDElementDeclarationImpl elementDeclaration = (XSDElementDeclarationImpl) ((XSDElementDeclarationAdapter) result) .getKey(); ComponentDefinition componentDefinition = null; // 1. Get component definition for directly attached // annotations. if (elementDeclaration.getAnnotation() != null) { componentDefinition = processAnnotations(element, elementDeclaration.getAnnotation() .getApplicationInformation(), parserContext); } // 2. If no directly attached annotation could be // found, try the referenced type definition if any. if (componentDefinition == null && elementDeclaration.getTypeDefinition() != null && elementDeclaration.getTypeDefinition().getAnnotation() != null) { componentDefinition = processAnnotations(element, elementDeclaration.getTypeDefinition().getAnnotation() .getApplicationInformation(), parserContext); } return componentDefinition; } } } } } catch (IOException e) { // Don't do anything. } catch (CoreException e) { // Don't do anything. } finally { // Make sure to release the StructuredModel again. if (model != null) { model.releaseFromRead(); } } return null; } /** * Parses a list of xsd:appinfo elements and looks for tool:annotations * @param element the root element * @param appInfos the appinfo elements found on the XSD definition for the element's type * @param parserContext the current parser context * @return a {@link ComponentDefinition} created from the given element */ private ComponentDefinition processAnnotations(Node element, List<Element> appInfos, ParserContext parserContext) { // Iterate the xsd:appinfo elements. for (Element elem : appInfos) { NodeList annotations = elem.getChildNodes(); // Iterate children for tool:annotation elements. for (int j = 0; j < annotations.getLength(); j++) { Node toolAnnotationElement = annotations.item(j); if (toolAnnotationElement.getNodeType() == Node.ELEMENT_NODE && ANNOTATION_ELEMENT.equals(toolAnnotationElement.getLocalName()) && TOOL_NAMESPACE_URI.equals(toolAnnotationElement.getNamespaceURI())) { NodeList specialToolAnnotationElements = toolAnnotationElement.getChildNodes(); // Iterate children for tool:exports elements. for (int k = 0; k < specialToolAnnotationElements.getLength(); k++) { Node specialToolAnnotation = specialToolAnnotationElements.item(k); if (specialToolAnnotation.getNodeType() == Node.ELEMENT_NODE && EXPORTS_ELEMENT.equals(specialToolAnnotation.getLocalName())) { return createBeanComponentDefinition(element, parserContext, specialToolAnnotation); } } } } } return null; } /** * Creates a {@link ComponentDefinition} based on the attribute values of the tool annotation. * @param element the element to create {@link ComponentDefinition} for * @param parserContext the current parser context * @param specialToolAnnotation the tool:exports annotation * @return the finally created {@link ComponentDefinition} */ protected ComponentDefinition createBeanComponentDefinition(Node element, ParserContext parserContext, Node specialToolAnnotation) { NamedNodeMap attributes = specialToolAnnotation.getAttributes(); String id = (attributes.getNamedItem(IDENTIFIER_ATTRIBUTE) != null ? attributes .getNamedItem(IDENTIFIER_ATTRIBUTE).getTextContent() : DEFAULT_ID_XPATH); String type = (attributes.getNamedItem(TYPE_ATTRIBUTE) != null ? attributes.getNamedItem( TYPE_ATTRIBUTE).getTextContent() : null); // Create the final BeanDefinition. GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClassName(type); beanDefinition.setSource(parserContext.extractSource(element)); // Make sure that this BeanDefinition is treated as INFRASTRUCTURE // so that it will not be validated and can be filtered. beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); return new BeanComponentDefinition(beanDefinition, getIdentifier(element, id, beanDefinition)); } /** * Gets the identifier for a {@link ComponentDefinition}. * <p> * First the given XPath expression will be evaluated against the given element. If this * evaluation does not return any useful value, the implementation will fall back to generate an * identifier based on {@link UniqueBeanNameGenerator#generateBeanName}. * @param element the element to evaluate the given identifier expression against * @param identifierExpression XPath expression to identify the identifier value * @param beanDefinition a created BeanDefinition required for the * {@link UniqueBeanNameGenerator} * @return a string identifier for the given element */ protected String getIdentifier(Node element, String identifierExpression, BeanDefinition beanDefinition) { String identifier = null; try { XPathFactory factory = XPathFactory.newInstance(); XPath path = factory.newXPath(); identifier = path.evaluate(identifierExpression, element); } catch (XPathExpressionException e) { // Safely ignore that here. } if (!StringUtils.hasText(identifier)) { identifier = UniqueBeanNameGenerator.generateBeanName(beanDefinition, beansConfig); } return identifier; } /** * Returns the {@link IStructuredModel} for the {@link IBeansConfig}-backing IFile. */ private IStructuredModel getStructuredModel() throws IOException, CoreException { IStructuredModel model; model = StructuredModelManager.getModelManager().getExistingModelForRead( (IFile) beansConfig.getElementResource()); if (model == null) { model = StructuredModelManager.getModelManager().getModelForRead( (IFile) beansConfig.getElementResource()); } return model; } }