001package org.andromda.maven.plugin.site;
002
003import java.io.ByteArrayInputStream;
004import java.io.ByteArrayOutputStream;
005import java.io.File;
006import java.io.FileWriter;
007import java.io.IOException;
008import java.io.InputStream;
009import java.net.URL;
010import java.util.List;
011import javax.xml.parsers.DocumentBuilder;
012import javax.xml.parsers.DocumentBuilderFactory;
013import javax.xml.parsers.ParserConfigurationException;
014import javax.xml.transform.Result;
015import javax.xml.transform.Source;
016import javax.xml.transform.TransformerFactory;
017import javax.xml.transform.dom.DOMSource;
018import javax.xml.transform.stream.StreamResult;
019import javax.xml.transform.stream.StreamSource;
020import org.apache.commons.lang.StringUtils;
021import org.dom4j.Element;
022import org.dom4j.io.SAXReader;
023import org.dom4j.io.XMLWriter;
024import org.w3c.dom.Document;
025import org.xml.sax.EntityResolver;
026import org.xml.sax.InputSource;
027import org.xml.sax.SAXException;
028
029/**
030 * Used to perform the transformation of XSL documents
031 * within the site plugin.
032 *
033 * @author Chad Brandon
034 * @author Vance Karimi
035 */
036public class XslTransformer
037{
038    private String projectName;
039
040    /**
041     * Default constructor
042     *
043     */
044    public XslTransformer()
045    {
046        // Default constructor
047    }
048
049    /**
050     * Constructor that sets the project name used to replace variable inside generated
051     * xdoc xml.
052     *
053     * @param projectNameIn
054     */
055    public XslTransformer(String projectNameIn)
056    {
057        this.projectName = projectNameIn;
058    }
059
060    /**
061     * Applies the given XSLT files to the model in the order in which they are found.
062     *
063     * @param xmlDocument The full path of the original XML
064     * @param transformation The full path of the XSLT
065     * @param outputLocation The full path of the output xdoc XML
066     */
067    public void transform(
068        final String xmlDocument,
069        final String transformation,
070        final String outputLocation)
071    {
072        try
073        {
074            this.transform(xmlDocument, new File(transformation).toURI().toURL(), outputLocation);
075        }
076        catch (final Exception exception)
077        {
078            throw new RuntimeException(exception);
079        }
080    }
081
082    /**
083     * Applies the given XSLT files to the model in the order in which they are found.
084     *
085     * @param xmlDocument The full path of the original XML
086     * @param xslt The URL of the XSLT
087     * @param outputLocation The full path of the output xdoc XML
088     */
089    public void transform(
090        final String xmlDocument,
091        final URL xslt,
092        final String outputLocation)
093    {
094        try
095        {
096            if (StringUtils.isNotBlank(xmlDocument))
097            {
098                final Source xmlSource = new DOMSource(this.urlToDocument(xmlDocument));
099                final TransformerFactory factory = TransformerFactory.newInstance();
100                if (xslt != null)
101                {
102                    final Source xsltSource = new StreamSource(xslt.openStream());
103                    final javax.xml.transform.Transformer transformer = factory.newTransformer(xsltSource);
104                    final ByteArrayOutputStream output = new ByteArrayOutputStream();
105                    final Result result = new StreamResult(output);
106                    transformer.transform(
107                        xmlSource,
108                        result);
109
110                    final byte[] outputResult = output.toByteArray();
111                    final org.dom4j.Document document = replaceVariableProperties(outputResult);
112
113                    if (StringUtils.isNotBlank(outputLocation))
114                    {
115                        final File fileOutput = new File(outputLocation);
116                        final File parent = fileOutput.getParentFile();
117                        if (parent != null)
118                        {
119                            parent.mkdirs();
120                        }
121
122                        FileWriter fwriter = new FileWriter(fileOutput);
123                        XMLWriter writer = new XMLWriter(fwriter);
124                        writer.write(document);
125                        writer.flush();
126                        writer.close();
127                        fwriter.close();
128                    }
129                }
130            }
131        }
132        catch (final Exception exception)
133        {
134            throw new RuntimeException(exception);
135        }
136    }
137
138    /**
139     * Parses the XML retrieved from the String URL and returns a Document object.
140     * @param url the url of the XML to parse.
141     * @return Document newly created Document object.
142     * @throws ParserConfigurationException
143     * @throws IOException
144     * @throws SAXException
145     */
146    @SuppressWarnings("static-method")
147    private Document urlToDocument(String url)
148        throws Exception
149    {
150        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
151        DocumentBuilder builder = factory.newDocumentBuilder();
152        builder.setEntityResolver(new XslTransformerEntityResolver(url));
153        return builder.parse(new InputSource(url));
154    }
155
156    /**
157     * The prefix that the systemId should start with when attempting
158     * to resolve it within a jar.
159     */
160    private static final String SYSTEM_ID_FILE = "file:";
161
162    /**
163     * Provides the resolution of external entities.
164     */
165    private static final class XslTransformerEntityResolver
166        implements EntityResolver
167    {
168        private String xmlDocument;
169
170        XslTransformerEntityResolver(final String xmlDocument)
171        {
172            this.xmlDocument = xmlDocument;
173        }
174
175        /**
176         * @see org.xml.sax.EntityResolver#resolveEntity(String, String)
177         */
178        @SuppressWarnings("resource")
179        public InputSource resolveEntity(
180            final String publicId,
181            final String systemId)
182            throws SAXException, IOException
183        {
184            InputSource source = null;
185            String path = systemId;
186            if (path != null && path.startsWith(SYSTEM_ID_FILE))
187            {
188                final String xmlResource = this.xmlDocument;
189                path = path.replaceFirst(
190                        SYSTEM_ID_FILE,
191                        "");
192
193                // - remove any extra starting slashes
194                path = path.replaceAll(
195                        "\\+",
196                        "/").replaceAll(
197                        "/+",
198                        "/");
199
200                // - if we still have one starting slash, remove it
201                if (path.startsWith("/"))
202                {
203                    path = path.substring(
204                            1,
205                            path.length());
206                }
207                final String xmlResourceName = xmlResource.replaceAll(
208                        ".*(\\+|/)",
209                        "");
210
211                InputStream inputStream = null;
212                URL uri = new File(StringUtils.replace(
213                            xmlResource,
214                            xmlResourceName,
215                            path)).toURI().toURL();
216                if (uri != null)
217                {
218                    inputStream = uri.openStream();
219                }
220                if (inputStream != null)
221                {
222                    source = new InputSource(inputStream);
223                    source.setPublicId(publicId);
224                    if (uri != null)
225                    {
226                        source.setSystemId(uri.toString());
227                    }
228                }
229            }
230            return source;
231        }
232    }
233
234    /**
235     * Replace the variable property defined by %module% in the output generated
236     * xdoc file.  Uses dom4j XPath to locate the variable.
237     *
238     * @param documentBuffer The byte array representing the xdoc XML
239     * @return the org.dom4j.Document object of the xdoc XML
240     * @throws Exception
241     */
242    private org.dom4j.Document replaceVariableProperties(byte[] documentBuffer)
243        throws Exception
244    {
245        SAXReader reader = new SAXReader();
246        org.dom4j.Document document = reader.read(new ByteArrayInputStream(documentBuffer));
247
248        // List elements = document.selectNodes("//*[contains(text(),'%module%')]");
249        List<Element> elements = document.selectNodes("//*");
250        for (final Element element : elements)
251        {
252            if (StringUtils.contains(element.getText(), "%module%"))
253            {
254                element.setText(
255                        StringUtils.replace(
256                                element.getText(),
257                                "%module%",
258                                this.getProjectName()));
259            }
260        }
261        elements.clear();
262
263        elements = document.selectNodes("//*[contains(@*,'%module%')]");
264        for (final Element element : elements)
265        {
266            element.addAttribute(
267                    "name",
268                    StringUtils.replace(
269                            element.attributeValue("name"),
270                            "%module%",
271                            this.getProjectName()));
272        }
273        return document;
274    }
275
276    /**
277     * @return Returns the projectName.
278     */
279    public String getProjectName()
280    {
281        return projectName;
282    }
283
284    /**
285     * @param projectName The projectName to set.
286     */
287    public void setProjectName(String projectName)
288    {
289        this.projectName = projectName;
290    }
291}