/* * 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.xml; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.util.Enumeration; import javax.xml.XMLConstants; import org.jboss.tools.smooks.templating.model.ModelBuilder; import org.jboss.tools.smooks.templating.model.ModelBuilder.ElementType; import org.jboss.tools.smooks.templating.model.ModelBuilderException; import org.jboss.tools.smooks.templating.model.ModelNodeResolver; import org.jboss.tools.smooks.templating.model.xml.XSDModelBuilder; import org.jboss.tools.smooks.templating.template.CollectionMapping; import org.jboss.tools.smooks.templating.template.Mapping; import org.jboss.tools.smooks.templating.template.TemplateBuilder; import org.jboss.tools.smooks.templating.template.ValueMapping; import org.jboss.tools.smooks.templating.template.exception.TemplateBuilderException; import org.jboss.tools.smooks.templating.template.freemarker.FreeMarkerTemplateBuilder; import org.jboss.tools.smooks.templating.template.util.FreeMarkerUtil; import org.milyn.xml.DomUtils; import org.milyn.xml.XmlUtil; import org.w3c.dom.Attr; 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; import freemarker.core.TemplateElement; import freemarker.template.Configuration; import freemarker.template.Template; /** * Freemarker Template Builder for an XML messages. * * @author <a href="mailto:tom.fennelly@jboss.com">tom.fennelly@jboss.com</a> */ public class XMLFreeMarkerTemplateBuilder extends FreeMarkerTemplateBuilder { private boolean omitXMLDeclaration = false; /** * Public constructor. * <p/> * This constructor is used when an existing template doesn't exist i.e. for creating new * templates with a new set of mappings. When creating an {@link XMLFreeMarkerTemplateBuilder} instance to incorporate * mappings from an existing template, use the {@link #XMLFreeMarkerTemplateBuilder(ModelBuilder, String)} * constructor. * * @param modelBuilder The {@link ModelBuilder} instance that describes the XML model (e.g. {@link XSDModelBuilder}). * @throws ModelBuilderException Invalid {@link ModelBuilder} instance. */ public XMLFreeMarkerTemplateBuilder(ModelBuilder modelBuilder) throws ModelBuilderException { super(modelBuilder); } /** * Public constructor. * <p/> * Used to construct an {@link XMLFreeMarkerTemplateBuilder} instance to incorporate mappings from an * existing FreeMarker template. * * @param modelBuilder The {@link ModelBuilder} instance that describes the XML model (e.g. {@link XSDModelBuilder}). * @param ftlTemplate FreeMarker template from which to extract existing {@link ValueMapping mappings}. * @throws ModelBuilderException Invalid {@link ModelBuilder} instance. * @throws TemplateBuilderException Error processing FreeMarker template. */ public XMLFreeMarkerTemplateBuilder(ModelBuilder modelBuilder, String ftlTemplate) throws ModelBuilderException, TemplateBuilderException { super(modelBuilder); addMappings(ftlTemplate); } /** * Omit the XML declaration at the start of the XML document. * <p/> * The XML declaration is added by default. * * @param omitXMLDeclaration True if the XML declaration is to be omitted, otherwise false. * @return This Template Builder instance. */ public TemplateBuilder setOmitXMLDeclaration(boolean omitXMLDeclaration) { this.omitXMLDeclaration = omitXMLDeclaration; return this; } public String buildTemplate() throws TemplateBuilderException { Writer templateWriter = new StringWriter(); if(!omitXMLDeclaration) { try { templateWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); //$NON-NLS-1$ } catch (IOException e) { throw new IllegalStateException("Unexpected IOException when writing the XML Template.", e); //$NON-NLS-1$ } } writeElement(getModel().getDocumentElement(), 0, false, templateWriter); return templateWriter.toString(); } /** * Recursively write the XML template elements to the supplied template writer. * @param element The element to write. * @param indent Indentation level. * @param templateWriter The writer. */ private void writeElement(Element element, int indent, boolean addNewline, Writer templateWriter) { try { if(ModelBuilder.isInReservedNamespace(element)) { writeElementChildren(element, indent, templateWriter); } else if(assertAddNodeToTemplate(element)) { if(addNewline) { templateWriter.write('\n'); } Mapping mapping = getMapping(element); CollectionMapping collectionMapping = null; if(mapping instanceof CollectionMapping) { collectionMapping = (CollectionMapping) mapping; TemplateBuilder.writeIndent(indent, templateWriter); templateWriter.write("<#list " + FreeMarkerUtil.toPath(collectionMapping.getSrcPath(), isNodeModelSource()) + " as " + collectionMapping.getCollectionItemName() + ">\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } TemplateBuilder.writeIndent(indent, templateWriter); writeElementStart(element, templateWriter); ChildWriteHistory writeChildHistory = writeElementChildren(element, indent + 1, templateWriter); if(writeChildHistory.numElementsWritten > 0) { TemplateBuilder.writeIndent(indent, templateWriter); } writeElementEnd(element, writeChildHistory.startClosed, templateWriter); if(collectionMapping != null) { templateWriter.write("\n"); //$NON-NLS-1$ TemplateBuilder.writeIndent(indent, templateWriter); templateWriter.write("</#list>"); //$NON-NLS-1$ } TemplateBuilder.writeIndent(indent, templateWriter); } } catch(IOException e) { throw new IllegalStateException("Unexpected IOException when writing the XML Template.", e); //$NON-NLS-1$ } } private void writeElementStart(Element element, Writer templateWriter) throws IOException { templateWriter.write("<"); //$NON-NLS-1$ templateWriter.write(element.getNodeName()); NamedNodeMap attributes = element.getAttributes(); for(int i = 0; i < attributes.getLength(); i++) { Attr attribute = (Attr) attributes.item(i); if(!ModelBuilder.isInReservedNamespace(attribute)) { Mapping mapping = getMapping(attribute); if(mapping != null) { writeAttribute(attribute.getNodeName(), FreeMarkerUtil.toFreeMarkerVariable((ValueMapping)mapping, isNodeModelSource()), templateWriter); //$NON-NLS-1$ } else if(ModelBuilder.isRequired(attribute)) { writeAttribute(attribute.getNodeName(), attribute.getValue(), templateWriter); } } } String nsURI = element.getNamespaceURI(); if(nsURI != null && !nsURI.equals(XMLConstants.NULL_NS_URI)) { String prefix = element.getPrefix(); if(!assertNamespaceDeclOnStack(prefix, nsURI, element)) { if(prefix != null) { writeAttribute(XMLConstants.XMLNS_ATTRIBUTE + ":" + prefix, nsURI, templateWriter); //$NON-NLS-1$ } else { writeAttribute(XMLConstants.XMLNS_ATTRIBUTE, nsURI, templateWriter); } } } } private void writeAttribute(String name, String value, Writer templateWriter) throws IOException { char quoteChar = (value.indexOf('\"') != -1) ? '\'' : '\"'; templateWriter.write(' '); //$NON-NLS-1$ templateWriter.write(name); templateWriter.write("=" + quoteChar); //$NON-NLS-1$ templateWriter.write(value); templateWriter.write(quoteChar); //$NON-NLS-1$ } private ChildWriteHistory writeElementChildren(Element element, int indent, Writer templateWriter) throws IOException { NodeList children = element.getChildNodes(); ChildWriteHistory writeHistory = new ChildWriteHistory(); if(children.getLength() == 0) { Mapping mapping = getMapping(element); if(ModelBuilder.getElementType(element) == ElementType.simple) { templateWriter.write(">"); //$NON-NLS-1$ writeHistory.startClosed = true; if(mapping != null) { templateWriter.write(FreeMarkerUtil.toFreeMarkerVariable((ValueMapping)mapping, isNodeModelSource())); } else { templateWriter.write(ModelBuilder.REQUIRED); } } } else { if(!ModelBuilder.isInReservedNamespace(element) && !writeHistory.startClosed) { templateWriter.write(">"); //$NON-NLS-1$ } writeHistory.startClosed = true; for(int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if(child.getNodeType() == Node.ELEMENT_NODE) { writeElement((Element)child, indent, true, templateWriter); writeHistory.numElementsWritten++; } } if(!ModelBuilder.isInReservedNamespace(element)) { templateWriter.write('\n'); //$NON-NLS-1$ } } return writeHistory; } private class ChildWriteHistory { private int numElementsWritten = 0; private boolean startClosed = false; } private void writeElementEnd(Element element, boolean startClosed, Writer templateWriter) throws IOException { if(startClosed) { templateWriter.write("</"); //$NON-NLS-1$ templateWriter.write(element.getNodeName()); templateWriter.write(">"); //$NON-NLS-1$ } else { templateWriter.write(" />"); //$NON-NLS-1$ } } private boolean assertNamespaceDeclOnStack(String prefix, String nsURI, Element element) { Node parent = element.getParentNode(); while (parent != null) { if(prefix == null) { if(parent.getPrefix() == null && nsURI.equals(parent.getNamespaceURI())) { return true; } } else if(prefix.equals(parent.getPrefix()) && nsURI.equals(parent.getNamespaceURI())) { return true; } parent = parent.getParentNode(); } return false; } private void addMappings(String ftlTemplate) throws TemplateBuilderException { Template template; try { template = new Template("ftlTemplate", new StringReader(ftlTemplate), new Configuration()); //$NON-NLS-1$ } catch (IOException e) { throw new TemplateBuilderException ("Failed to parse the Supplied FreeMarker template.", e); //$NON-NLS-1$ } TemplateElement rootElement = template.getRootTreeNode(); StringWriter templateRewriteBuffer = new StringWriter(); rewriteTemplateElement(rootElement, templateRewriteBuffer); Document templateDOM; try { templateDOM = XmlUtil.parseStream(new StringReader(templateRewriteBuffer.toString())); } catch (Exception e) { throw new TemplateBuilderException("Error parsing rewritten FreeMarker template.", e); //$NON-NLS-1$ } addMappings(templateDOM.getDocumentElement(), new ModelNodeResolver(getModel())); } private void addMappings(Element element, ModelNodeResolver modelNodeResolver) throws TemplateBuilderException { if(TemplateBuilder.isListElement(element)) { addCollectionMapping(element, modelNodeResolver); } else if(!ModelBuilder.isInReservedNamespace(element)) { String elementText = DomUtils.getAllText(element, false); // Handle the element itself... if(FreeMarkerUtil.isDollarVariable(elementText)) { addValueMapping(element, modelNodeResolver, elementText); } // Add mappings for the attributes... NamedNodeMap attributes = element.getAttributes(); int attribCount = attributes.getLength(); for(int i = 0; i < attribCount; i++) { Attr attribute = (Attr) attributes.item(i); if(!ModelBuilder.isInReservedNamespace(attribute)) { String attrValue = attribute.getValue(); if(FreeMarkerUtil.isDollarVariable(attrValue)) { addValueMapping(attribute, modelNodeResolver, attrValue); } } } } // Iterate down into the child elements... 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) { addMappings((Element) child, modelNodeResolver); } } } private void rewriteTemplateElement(TemplateElement element, StringWriter templateRewriteBuffer) throws TemplateBuilderException { // We need to rewrite the FreeMarker template so as to get rid of the FreeMarker constructs, // such as <#list> etc. We convert these to XML elements in the ModelBuilder.NAMESPACE namespace. if(element.getNodeName().equals("IteratorBlock")) { //$NON-NLS-1$ String description = element.getDescription(); if(!description.startsWith("list")) { //$NON-NLS-1$ throw new TemplateBuilderException ("Unsupported XML template IteratorBlock type '" + description + "'. Currently only support 'list' IteratorBlock nodes."); //$NON-NLS-1$ //$NON-NLS-2$ } String[] tokens = description.split(" +?"); //$NON-NLS-1$ TemplateBuilder.writeListStart(templateRewriteBuffer, FreeMarkerUtil.toPath(tokens[1], isNodeModelSource()), tokens[3]); Enumeration<TemplateElement> children = element.children(); while(children != null && children.hasMoreElements()) { rewriteTemplateElement(children.nextElement(), templateRewriteBuffer); } TemplateBuilder.writeListEnd(templateRewriteBuffer); } else { Enumeration<TemplateElement> children = element.children(); if(children != null && children.hasMoreElements()) { while(children.hasMoreElements()) { rewriteTemplateElement(children.nextElement(), templateRewriteBuffer); } } else { if(element.getClass().getSimpleName().equals("DollarVariable")) { //$NON-NLS-1$ templateRewriteBuffer.append(element.toString()); } else { templateRewriteBuffer.append(element.getCanonicalForm()); } } } } }