/*
* 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.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 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.model.ModelBuilder.ElementType;
import org.jboss.tools.smooks.templating.model.xml.XSDModelBuilder;
import org.jboss.tools.smooks.templating.template.*;
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 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());
}
}
}
}
}