/* * JBoss, Home of Professional Open Source * Copyright 2006, JBoss Inc., and others contributors as indicated * by the @authors tag. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * 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, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. * * (C) 2005-2006, JBoss Inc. */ package org.jboss.tools.smooks.templating.template; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Properties; import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.eclipse.core.runtime.Assert; import org.jboss.tools.smooks.templating.model.ModelBuilder; import org.jboss.tools.smooks.templating.model.ModelBuilderException; import org.jboss.tools.smooks.templating.model.ModelNodeResolver; import org.jboss.tools.smooks.templating.template.exception.InvalidMappingException; import org.jboss.tools.smooks.templating.template.exception.TemplateBuilderException; import org.jboss.tools.smooks.templating.template.exception.UnmappedCollectionNodeException; import org.jboss.tools.smooks.templating.template.result.AddCollectionResult; import org.jboss.tools.smooks.templating.template.result.RemoveResult; import org.jboss.tools.smooks.templating.template.util.FreeMarkerUtil; import org.milyn.xml.DomUtils; 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; /** * Abstract Template Builder. * <p/> * See <a href="http://www.jboss.org/community/wiki/SmooksEditorTemplateGeneration">Wiki Docs</a>. * * @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a> */ public abstract class TemplateBuilder { private ModelBuilder modelBuilder; private Document model; private List<Mapping> mappings = new ArrayList<Mapping>(); private XPathFactory xpathFactory = XPathFactory.newInstance(); private XPathNamespaceContext namespaceContext; /** * Public constructor. * * @param modelBuilder * The model builder underlying this template builder instance. * @throws ModelBuilderException * Error building model. */ public TemplateBuilder(ModelBuilder modelBuilder) throws ModelBuilderException { Assert.isNotNull(modelBuilder, "modelBuilder"); //$NON-NLS-1$ this.modelBuilder = modelBuilder; this.model = modelBuilder.buildModel(); this.namespaceContext = new XPathNamespaceContext(modelBuilder.getNamespaces()); } /** * Build the template for the specified model, based on the supplied * mappings. * * @return The template. * @throws org.jboss.tools.smooks.templating.template.exception.TemplateBuilderException * Exception building template. */ public abstract String buildTemplate() throws TemplateBuilderException; /** * Get the model associated with the template. * * @return The model. */ public Document getModel() { return model; } /** * Get the model DOM Node specified by the supplied XPath expression * * @param xpathExpr * The XPath expression. * @return The Model node specified by the XPath expression, or null. * @throws XPathExpressionException * error evaluating XPath expression. */ public Node getModelNode(String xpathExpr) throws XPathExpressionException { XPath xpath = xpathFactory.newXPath(); xpath.setNamespaceContext(namespaceContext); return (Node) xpath.evaluate(xpathExpr, model, XPathConstants.NODE); } /** * Get the {@link ModelBuilder} instance associated with this * {@link TemplateBuilder}. * * @return The {@link ModelBuilder} instance associated with this * {@link TemplateBuilder}. */ public ModelBuilder getModelBuilder() { return modelBuilder; } /** * Add a source to model value mapping. * * @param srcPath * Source path. Depends on the source type e.g. will be a java * object graph path for a bean property and will be an xml path * for an XML source. * @param modelPath * The mapping path in the target model. * @return The mapping instance. * @throws InvalidMappingException * Invalid mapping. */ public ValueMapping addValueMapping(String srcPath, Node modelPath) throws InvalidMappingException { asserValidMappingNode(modelPath); assertCollectionsMapped(modelPath); ValueMapping mapping = new ValueMapping(srcPath, modelPath); mappings.add(mapping); addHideNodes(modelPath, mapping); return mapping; } /** * Add a source to model collection mapping. * * @param srcCollectionPath * Source path. * @param modelCollectionPath * The mapping path in the target model. * @param collectionItemName * The name associated with the individual collection items. * @return The mapping instance. * @throws InvalidMappingException * Invalid mapping. */ public AddCollectionResult addCollectionMapping(String srcCollectionPath, Element modelCollectionPath, String collectionItemName) throws InvalidMappingException { asserValidMappingNode(modelCollectionPath); assertCollectionsMapped(modelCollectionPath.getParentNode()); CollectionMapping mapping = new CollectionMapping(srcCollectionPath, modelCollectionPath, collectionItemName); mappings.add(mapping); addHideNodes(modelCollectionPath, mapping); List<Mapping> removeMappings = new ArrayList<Mapping>(); findChildMappings(modelCollectionPath, mapping, parseSourcePath(mapping), removeMappings); AddCollectionResult result = new AddCollectionResult(mapping, removeMappings); return result; } /** * Remove the specified mapping. * * @param mapping The mapping instance to be removed. * @return The remove mapping result. */ public RemoveResult removeMapping(Mapping mapping) { List<Node> showNodes = new ArrayList<Node>(); mappings.remove(mapping); List<Node> hideNodes = mapping.getHideNodes(); if (hideNodes != null) { for (Node hiddenNode : hideNodes) { if (hiddenNode.getNodeType() == Node.ELEMENT_NODE) { if (!isOnMappingPath((Element) hiddenNode)) { ModelBuilder.unhideFragment((Element) hiddenNode); showNodes.add(hiddenNode); } } } } // If the mapping is a collection mapping, we need to remove all child mappings... List<Mapping> removeMappings = new ArrayList<Mapping>(); if(mapping instanceof CollectionMapping) { findChildMappings((Element)mapping.getMappingNode(), (CollectionMapping)mapping, parseSourcePath(mapping), removeMappings); } return new RemoveResult(removeMappings, showNodes); } private void addHideNodes(Node modelPath, Mapping mapping) { Node parent = ModelBuilder.getParentNode(modelPath); while (parent != null) { if (ModelBuilder.isCompositor(parent)) { Element compositor = (Element) parent; int maxOccurs = ModelBuilder.getMaxOccurs(compositor); int numElementsOnMappingPath = getNumElementsOnMappingPath(compositor); if (numElementsOnMappingPath == maxOccurs) { hideUnmappedPaths(compositor, mapping); } } parent = ModelBuilder.getParentNode(parent); } } private void findChildMappings(Element modelPath, CollectionMapping collectionMapping, String[] srcPathTokens, List<Mapping> mappings) { // Find any Mappings to nodes inside the collection mapping node, // where that mapping's source path is also inside the mapping source path of // the supplied collection mapping... // Check the attributes... NamedNodeMap attributes = modelPath.getAttributes(); int attribCount = attributes.getLength(); for(int i = 0; i < attribCount; i++) { Node attribNode = attributes.item(i); Mapping attribMapping = getMapping(attribNode); if(attribMapping != null && attribMapping != collectionMapping) { String[] attribMappingSrcPathTokens = parseSourcePath(attribMapping); if(isChildSourceMapping(attribMappingSrcPathTokens, srcPathTokens)) { mappings.add(attribMapping); } } } // Check the child elements, drilling down recursively ... NodeList childNodes = modelPath.getChildNodes(); int childCount = childNodes.getLength(); for(int i = 0; i < childCount; i++) { Node childNode = childNodes.item(i); if(childNode.getNodeType() == Node.ELEMENT_NODE) { Mapping childMapping = getMapping(childNode); if(childMapping != null && childMapping != collectionMapping) { String[] childMappingSrcPathTokens = parseSourcePath(childMapping); if(childMappingSrcPathTokens.length > 0 && childMappingSrcPathTokens[0].equals(collectionMapping.getCollectionItemName())) { mappings.add(childMapping); } else if(isChildSourceMapping(childMappingSrcPathTokens, srcPathTokens)) { mappings.add(childMapping); } } // Drill down recursively... findChildMappings((Element) childNode, collectionMapping, srcPathTokens, mappings); } } } private boolean isChildSourceMapping(String[] childSrcPathTokens, String[] srcPathTokens) { if(childSrcPathTokens.length < srcPathTokens.length) { return false; } for(int i = 0; i < srcPathTokens.length; i++) { if(!srcPathTokens[i].equals(childSrcPathTokens[i])) { return false; } } return true; } private void hideUnmappedPaths(Element compositor, Mapping mapping) { NodeList children = compositor.getChildNodes(); int numChildren = children.getLength(); for (int i = 0; i < numChildren; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Element nodeToHide = (Element) child; if (!isOnMappingPath(nodeToHide)) { mapping.addHideNode(nodeToHide); ModelBuilder.hideFragment(nodeToHide); } } } } private int getNumElementsOnMappingPath(Element compositor) { NodeList children = compositor.getChildNodes(); int numChildren = children.getLength(); int count = 0; for (int i = 0; i < numChildren; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { if (isOnMappingPath((Element) child)) { count++; } } } return count; } /** * Get the full list of mappings. * * @return The full list of mappings. */ public List<Mapping> getMappings() { return mappings; } private void asserValidMappingNode(Node mappingNode) throws InvalidMappingException { if (mappingNode == null) { throw new InvalidMappingException("Node is null."); //$NON-NLS-1$ } if (mappingNode.getNodeType() != Node.ATTRIBUTE_NODE && mappingNode.getNodeType() != Node.ELEMENT_NODE) { throw new InvalidMappingException( "Unsupported XML target node mapping. Support XML elements and attributes only."); //$NON-NLS-1$ } if (ModelBuilder.NAMESPACE.equals(mappingNode.getNamespaceURI())) { throw new InvalidMappingException( "Unsupported XML target node mapping. Cannot map to a reserved model node from the '" + ModelBuilder.NAMESPACE + "' namespace."); //$NON-NLS-1$ //$NON-NLS-2$ } if (ModelBuilder.isHidden(mappingNode)) { throw new InvalidMappingException( "Illegal XML target node mapping for node '" + mappingNode + "'. This node (or one of it's ancestors) is hidden."); //$NON-NLS-1$ //$NON-NLS-2$ } } private void assertCollectionsMapped(Node mappingNode) throws UnmappedCollectionNodeException { Element collectionElement = getNearestCollectionElement(mappingNode); if (collectionElement != null) { CollectionMapping parentCollectionMapping = getCollectionMapping(collectionElement); if (parentCollectionMapping == null && ModelBuilder.getEnforceCollectionSubMappingRules(collectionElement)) { throw new UnmappedCollectionNodeException(collectionElement); } } } /** * Get the nearest "Collection" element to for the supplied model node. * <p/> * This can be the node itself, if it is a Collection node. * * @param modelPath * The starting point of the search. * @return The nearest Collection element (possibly itself), or null if * there is non. */ public Element getNearestCollectionElement(Node modelPath) { Node nextNode = modelPath; while (nextNode != null) { if (nextNode.getNodeType() == Node.ELEMENT_NODE) { if(ModelBuilder.isCollection((Element) nextNode)) { return (Element) nextNode; } } nextNode = ModelBuilder.getParentNode(nextNode); } return null; } protected CollectionMapping getCollectionMapping(Element collectionElement) { Mapping mapping = getMapping(collectionElement); if (mapping instanceof CollectionMapping) { return (CollectionMapping) mapping; } return null; } protected Mapping getMapping(Node node) { for (Mapping mapping : mappings) { if (mapping.getMappingNode() == node) { return mapping; } } return null; } /** * Assert whether or not the specified model node details should be added to * the template. * * @param element * The model element. * @return True if the model node details should be added to the template, * otherwise false. */ protected boolean assertAddNodeToTemplate(Element element) { // Don't write the element if any of the following are true: // 1. Is from the reserved namespace (ModelBuilder.NAMESPACE). // 2. Is hidden. // 3. Has a minOccurs of 0 and has no mappings onto it (including any of // it's attributes and // child elements) i.e. it is not a parent of any of the mappings. if (ModelBuilder.isInReservedNamespace(element)) { return false; } else if (ModelBuilder.isHidden(element)) { return false; } else if (ModelBuilder.getMinOccurs(element) == 0 && !isOnMappingPath(element)) { return false; } return true; } /** * Is the supplied element on the path of any of the currently mapped model * nodes. * * @param element * The element to be checked. * @return True if the element is on the path of one of the Mappings, * otherwise false. */ public boolean isOnMappingPath(Element element) { for (Mapping mapping : mappings) { Node pathNode = mapping.getMappingNode(); while (pathNode != null) { if (element == pathNode) { return true; } pathNode = ModelBuilder.getParentNode(pathNode); } } return false; } /** * Add a collection mapping from the supplied template model element and * target {@link ModelNodeResolver}. * * @param element * The element from the template model (DOM model built from a * template). * @param modelNodeResolver * The target model node resolver. * @throws TemplateBuilderException * Invalid collection element. */ protected void addCollectionMapping(Element element, ModelNodeResolver modelNodeResolver) throws TemplateBuilderException { NodeList children = element.getChildNodes(); int childCount = children.getLength(); for (int i = 0; i < childCount; i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { Node targetModelNode = modelNodeResolver.resolveNodeMapping(child); String srcPath = element.getAttributeNS(ModelBuilder.NAMESPACE, "srcPath"); //$NON-NLS-1$ String collectionItemName = element.getAttributeNS(ModelBuilder.NAMESPACE, "collectionItemName"); //$NON-NLS-1$ addCollectionMapping(srcPath, (Element) targetModelNode, collectionItemName); return; } } throw new TemplateBuilderException( "Unexpected Exception. Invalid <smk:list> collection node. Has no child elements!"); //$NON-NLS-1$ } protected void addValueMapping(Node modelNode, ModelNodeResolver modelNodeResolver, String dollarVariable) throws TemplateBuilderException, InvalidMappingException { Node targetModelNode = modelNodeResolver.resolveNodeMapping(modelNode); addValueMapping(targetModelNode, dollarVariable); } protected void addValueMapping(Node modelNode, String dollarVariable) throws TemplateBuilderException, InvalidMappingException { // TODO: Need to get all FreeMarker specific code out of here and pushed down into the FreeMarkerTemplateBuilder class String srcPath = FreeMarkerUtil.extractJavaPath(dollarVariable); String rawFormatting = FreeMarkerUtil.extractRawFormatting(dollarVariable); ValueMapping mapping = addValueMapping(srcPath, modelNode); if(rawFormatting != null) { Properties encodeProperties = new Properties(); encodeProperties.setProperty(ValueMapping.RAW_FORMATING_KEY, rawFormatting); mapping.setEncodeProperties(encodeProperties); } } /** * Resolves the full model source path for the specified mapping. * <p/> * Takes enclosing {@link CollectionMappings} into account. * @param mapping The mapping. * @return The fully resolved path. */ public String resolveMappingSrcPath(Mapping mapping) { String[] srcPathTokens = parseSourcePath(mapping); if(srcPathTokens.length > 1) { CollectionMapping parentCollection = findParentCollection(srcPathTokens[0], mapping); if(parentCollection != null) { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(resolveMappingSrcPath(parentCollection)); for(int i = 1; i < srcPathTokens.length; i++) { pathBuilder.append('/'); pathBuilder.append(srcPathTokens[i]); } return pathBuilder.toString(); } } // No parent collection, so just pass back the path... return mapping.getSrcPath(); } protected String[] parseSourcePath(Mapping mapping) { return mapping.getSrcPath().split("/"); } public CollectionMapping findParentCollection(String collectionName, Mapping mapping) { CollectionMapping parentCollection = findCollection(collectionName); if(parentCollection != null) { if(parentCollection.isParentNodeMapping(mapping)) { return parentCollection; } } return null; } public CollectionMapping findCollection(String collectionName) { for(Mapping mapping : mappings) { if(mapping instanceof CollectionMapping && ((CollectionMapping) mapping).getCollectionItemName().equals(collectionName)) { return (CollectionMapping) mapping; } } return null; } public static void writeListStart(StringWriter writer, String srcPath, String collectionItemName) { writer.write("<smk:list smk:srcPath='" + srcPath + "' smk:collectionItemName=\"" + collectionItemName + "\" xmlns:smk=\"" + ModelBuilder.NAMESPACE + "\">"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ } public static void writeListEnd(StringWriter writer) { writer.write("</smk:list>"); //$NON-NLS-1$ } public static boolean isListElement(Element element) { if (ModelBuilder.isInReservedNamespace(element)) { if (DomUtils.getName(element).equals("list")) { //$NON-NLS-1$ return true; } } return false; } protected static void writeIndent(int indent, Writer writer) { try { // 1 indent == 4 spaces... for (int i = 0; i < indent * 4; i++) { writer.write(' '); } } catch (IOException e) { throw new IllegalStateException("Unexpected IOException writing template.", e); //$NON-NLS-1$ } } private class XPathNamespaceContext implements NamespaceContext { private Properties namespaces; private XPathNamespaceContext(Properties namespaces) { this.namespaces = namespaces; } public String getNamespaceURI(String prefix) { if (prefix.equals("smk")) { //$NON-NLS-1$ return ModelBuilder.NAMESPACE; } else { return namespaces.getProperty(prefix); } } public String getPrefix(String namespaceURI) { return null; } public Iterator getPrefixes(String namespaceURI) { return null; } } }