XslTransformer.java

package org.andromda.maven.plugin.site;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import org.w3c.dom.Document;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Used to perform the transformation of XSL documents
 * within the site plugin.
 *
 * @author Chad Brandon
 * @author Vance Karimi
 */
public class XslTransformer
{
    private String projectName;

    /**
     * Default constructor
     *
     */
    public XslTransformer()
    {
        // Default constructor
    }

    /**
     * Constructor that sets the project name used to replace variable inside generated
     * xdoc xml.
     *
     * @param projectNameIn
     */
    public XslTransformer(String projectNameIn)
    {
        this.projectName = projectNameIn;
    }

    /**
     * Applies the given XSLT files to the model in the order in which they are found.
     *
     * @param xmlDocument The full path of the original XML
     * @param transformation The full path of the XSLT
     * @param outputLocation The full path of the output xdoc XML
     */
    public void transform(
        final String xmlDocument,
        final String transformation,
        final String outputLocation)
    {
        try
        {
            this.transform(xmlDocument, new File(transformation).toURI().toURL(), outputLocation);
        }
        catch (final Exception exception)
        {
            throw new RuntimeException(exception);
        }
    }

    /**
     * Applies the given XSLT files to the model in the order in which they are found.
     *
     * @param xmlDocument The full path of the original XML
     * @param xslt The URL of the XSLT
     * @param outputLocation The full path of the output xdoc XML
     */
    public void transform(
        final String xmlDocument,
        final URL xslt,
        final String outputLocation)
    {
        try
        {
            if (StringUtils.isNotBlank(xmlDocument))
            {
                final Source xmlSource = new DOMSource(this.urlToDocument(xmlDocument));
                final TransformerFactory factory = TransformerFactory.newInstance();
                if (xslt != null)
                {
                    final Source xsltSource = new StreamSource(xslt.openStream());
                    final javax.xml.transform.Transformer transformer = factory.newTransformer(xsltSource);
                    final ByteArrayOutputStream output = new ByteArrayOutputStream();
                    final Result result = new StreamResult(output);
                    transformer.transform(
                        xmlSource,
                        result);

                    final byte[] outputResult = output.toByteArray();
                    final org.dom4j.Document document = replaceVariableProperties(outputResult);

                    if (StringUtils.isNotBlank(outputLocation))
                    {
                        final File fileOutput = new File(outputLocation);
                        final File parent = fileOutput.getParentFile();
                        if (parent != null)
                        {
                            parent.mkdirs();
                        }

                        FileWriter fwriter = new FileWriter(fileOutput);
                        XMLWriter writer = new XMLWriter(fwriter);
                        writer.write(document);
                        writer.flush();
                        writer.close();
                        fwriter.close();
                    }
                }
            }
        }
        catch (final Exception exception)
        {
            throw new RuntimeException(exception);
        }
    }

    /**
     * Parses the XML retrieved from the String URL and returns a Document object.
     * @param url the url of the XML to parse.
     * @return Document newly created Document object.
     * @throws ParserConfigurationException
     * @throws IOException
     * @throws SAXException
     */
    @SuppressWarnings("static-method")
    private Document urlToDocument(String url)
        throws Exception
    {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        builder.setEntityResolver(new XslTransformerEntityResolver(url));
        return builder.parse(new InputSource(url));
    }

    /**
     * The prefix that the systemId should start with when attempting
     * to resolve it within a jar.
     */
    private static final String SYSTEM_ID_FILE = "file:";

    /**
     * Provides the resolution of external entities.
     */
    private static final class XslTransformerEntityResolver
        implements EntityResolver
    {
        private String xmlDocument;

        XslTransformerEntityResolver(final String xmlDocument)
        {
            this.xmlDocument = xmlDocument;
        }

        /**
         * @see org.xml.sax.EntityResolver#resolveEntity(String, String)
         */
        @SuppressWarnings("resource")
        public InputSource resolveEntity(
            final String publicId,
            final String systemId)
            throws SAXException, IOException
        {
            InputSource source = null;
            String path = systemId;
            if (path != null && path.startsWith(SYSTEM_ID_FILE))
            {
                final String xmlResource = this.xmlDocument;
                path = path.replaceFirst(
                        SYSTEM_ID_FILE,
                        "");

                // - remove any extra starting slashes
                path = path.replaceAll(
                        "\\+",
                        "/").replaceAll(
                        "/+",
                        "/");

                // - if we still have one starting slash, remove it
                if (path.startsWith("/"))
                {
                    path = path.substring(
                            1,
                            path.length());
                }
                final String xmlResourceName = xmlResource.replaceAll(
                        ".*(\\+|/)",
                        "");

                InputStream inputStream = null;
                URL uri = new File(StringUtils.replace(
                            xmlResource,
                            xmlResourceName,
                            path)).toURI().toURL();
                if (uri != null)
                {
                    inputStream = uri.openStream();
                }
                if (inputStream != null)
                {
                    source = new InputSource(inputStream);
                    source.setPublicId(publicId);
                    if (uri != null)
                    {
                        source.setSystemId(uri.toString());
                    }
                }
            }
            return source;
        }
    }

    /**
     * Replace the variable property defined by %module% in the output generated
     * xdoc file.  Uses dom4j XPath to locate the variable.
     *
     * @param documentBuffer The byte array representing the xdoc XML
     * @return the org.dom4j.Document object of the xdoc XML
     * @throws Exception
     */
    private org.dom4j.Document replaceVariableProperties(byte[] documentBuffer)
        throws Exception
    {
        SAXReader reader = new SAXReader();
        org.dom4j.Document document = reader.read(new ByteArrayInputStream(documentBuffer));

        // List elements = document.selectNodes("//*[contains(text(),'%module%')]");
        List<Element> elements = document.selectNodes("//*");
        for (final Element element : elements)
        {
            if (StringUtils.contains(element.getText(), "%module%"))
            {
                element.setText(
                        StringUtils.replace(
                                element.getText(),
                                "%module%",
                                this.getProjectName()));
            }
        }
        elements.clear();

        elements = document.selectNodes("//*[contains(@*,'%module%')]");
        for (final Element element : elements)
        {
            element.addAttribute(
                    "name",
                    StringUtils.replace(
                            element.attributeValue("name"),
                            "%module%",
                            this.getProjectName()));
        }
        return document;
    }

    /**
     * @return Returns the projectName.
     */
    public String getProjectName()
    {
        return projectName;
    }

    /**
     * @param projectName The projectName to set.
     */
    public void setProjectName(String projectName)
    {
        this.projectName = projectName;
    }
}