001/*
002  GRANITE DATA SERVICES
003  Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004
005  This file is part of Granite Data Services.
006
007  Granite Data Services is free software; you can redistribute it and/or modify
008  it under the terms of the GNU Library General Public License as published by
009  the Free Software Foundation; either version 2 of the License, or (at your
010  option) any later version.
011
012  Granite Data Services is distributed in the hope that it will be useful, but
013  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015  for more details.
016
017  You should have received a copy of the GNU Library General Public License
018  along with this library; if not, see <http://www.gnu.org/licenses/>.
019*/
020
021package org.granite.util;
022
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.io.StringReader;
027import java.io.StringWriter;
028import java.lang.reflect.Method;
029import java.nio.charset.Charset;
030import java.util.ArrayList;
031import java.util.List;
032
033import javax.xml.parsers.DocumentBuilder;
034import javax.xml.parsers.DocumentBuilderFactory;
035import javax.xml.parsers.ParserConfigurationException;
036import javax.xml.transform.OutputKeys;
037import javax.xml.transform.Templates;
038import javax.xml.transform.Transformer;
039import javax.xml.transform.TransformerException;
040import javax.xml.transform.TransformerFactory;
041import javax.xml.transform.dom.DOMSource;
042import javax.xml.transform.stream.StreamResult;
043import javax.xml.transform.stream.StreamSource;
044import javax.xml.xpath.XPathConstants;
045import javax.xml.xpath.XPathFactory;
046
047import org.w3c.dom.Document;
048import org.w3c.dom.Element;
049import org.w3c.dom.Node;
050import org.w3c.dom.NodeList;
051import org.xml.sax.EntityResolver;
052import org.xml.sax.ErrorHandler;
053import org.xml.sax.InputSource;
054import org.xml.sax.SAXException;
055
056/**
057 * @author Franck WOLFF
058 */
059public class StdXMLUtil implements XMLUtil {
060
061        protected static final String TO_STRING_XSL = 
062                "<?xml version='1.0' encoding='UTF-8'?>" +
063                "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>" +
064                "    <xsl:strip-space elements='*'/>" +
065                "    <xsl:template match='/'>" +
066                "        <xsl:copy-of select='*'/>" +
067                "    </xsl:template>" +
068                "</xsl:stylesheet>";
069
070    private DocumentBuilderFactory documentBuilderFactory = null;
071        private DocumentBuilderFactory validatingDocumentBuilderFactory = null;
072    private TransformerFactory transformerFactory = null;
073        private Templates toStringTemplates = null;
074        private XPathFactory xPathFactory = null;
075
076        
077        public Document newDocument() {
078        return newDocument(null);
079        }
080        
081        public Document newDocument(String root) {
082                try {
083                        Document document = getDocumentBuilderFactory().newDocumentBuilder().newDocument();
084                        document.setXmlVersion("1.0");
085                document.setXmlStandalone(true);
086                if (root != null)
087                                newElement(document, root);
088                return document;
089                }
090                catch (Exception e) {
091                        throw new RuntimeException(e);
092                }
093        }
094        
095        public Document getDocument(Node node) {
096                return (node instanceof Document ? (Document)node : node.getOwnerDocument());
097        }
098
099        public Element newElement(Node parent, String name) {
100                return newElement(parent, name, null);
101        }
102
103        public Element newElement(Node parent, String name, String value) {
104                Element element = getDocument(parent).createElement(name);
105                parent.appendChild(element);
106                if (value != null)
107                        element.setTextContent(value);
108                return element;
109        }
110        
111        public String getNormalizedValue(Node node) {
112                if (node == null)
113                        return null;
114                if (node.getNodeType() == Node.ELEMENT_NODE) {
115                        StringBuilder sb = new StringBuilder();
116                        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
117                                if (child.getNodeType() == Node.TEXT_NODE && child.getNodeValue() != null) {
118                                        String value = child.getNodeValue().trim();
119                                        if (value.length() > 0) {
120                                                if (sb.length() > 0)
121                                                        sb.append(' ');
122                                                sb.append(value);
123                                        }
124                                }
125                        }
126                        return sb.toString();
127                }
128                return (node.getNodeValue() != null ? node.getNodeValue().trim() : null);
129        }
130
131        public String setValue(Node node, String value) {
132                if (node != null) {
133                        String previousValue = getNormalizedValue(node);
134                        switch (node.getNodeType()) {
135                        case Node.ELEMENT_NODE:
136                                ((Element)node).setTextContent(value);
137                                break;
138                        case Node.ATTRIBUTE_NODE:
139                        case Node.TEXT_NODE:
140                                node.setNodeValue(value);
141                                break;
142                        default:
143                                throw new RuntimeException("Illegal node for write operations: " + node);
144                        }
145                        return previousValue;
146                }
147                return null;
148        }
149        
150    public Document buildDocument(String xml) {
151        try {
152            DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder();
153            return builder.parse(new InputSource(new StringReader(xml)));
154        } catch (Exception e) {
155            throw new RuntimeException("Could not parse XML string", e);
156        }
157    }
158    
159        public Document loadDocument(InputStream input) throws IOException, SAXException {
160                try {
161                        return getDocumentBuilderFactory().newDocumentBuilder().parse(input);
162                } catch (ParserConfigurationException e) {
163                        throw new RuntimeException(e);
164                }
165        }
166        
167        public Document loadDocument(InputStream input, EntityResolver resolver, ErrorHandler errorHandler) throws IOException, SAXException {
168                try {
169                        DocumentBuilder builder = getValidatingDocumentBuilderFactory().newDocumentBuilder();
170                        builder.setEntityResolver(resolver);
171                        if (errorHandler != null)
172                                builder.setErrorHandler(errorHandler);
173                        return builder.parse(input);
174                } catch (ParserConfigurationException e) {
175                        throw new RuntimeException(e);
176                }
177        }       
178    
179        public void saveDocument(Document document, OutputStream output) {
180                try {
181                        Transformer transformer = TransformerFactory.newInstance().newTransformer();
182                        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
183                        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
184                        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
185                        transformer.setOutputProperty(OutputKeys.INDENT, "no");
186                        
187                        transformer.transform(new DOMSource(document), new StreamResult(output));
188                }
189                catch (TransformerException e) {
190                        throw new RuntimeException("Could not save document", e);
191                }
192        }
193
194    public String toString(Document doc) {
195        try {
196            Transformer transformer = getTransformerFactory().newTransformer();
197            StringWriter writer = new StringWriter();
198            transformer.transform(new DOMSource(doc), new StreamResult(writer));
199            return writer.toString();
200        } catch (Exception e) {
201            throw new RuntimeException("Could not serialize document", e);
202        }
203    }
204    
205        protected Templates getToStringTemplates() {
206                if (toStringTemplates == null) {
207                        try {
208                                toStringTemplates = TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(TO_STRING_XSL)));
209                        } catch (Exception e) {
210                                throw new RuntimeException(e);
211                        }
212                }
213                return toStringTemplates;
214        }
215
216        public String toNodeString(Node node) {
217                try {
218                        Transformer transformer = getToStringTemplates().newTransformer();
219                        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
220                        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
221                        transformer.setOutputProperty(OutputKeys.ENCODING, Charset.defaultCharset().name());
222                        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
223                        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
224                        
225                        StringWriter sw = new StringWriter();
226                        transformer.transform(new DOMSource(node), new StreamResult(sw));
227                        return sw.toString();
228                } catch (Exception e) {
229                        throw new RuntimeException(e);
230                }
231                                
232        }
233        
234        public Node selectSingleNode(Object context, String expression) {
235                try {
236                        return (Node)getXPathFactory().newXPath().evaluate(expression, context, XPathConstants.NODE);
237                } catch (Exception e) {
238                        throw new RuntimeException(e);
239                }
240        }
241        
242        public List<Node> selectNodeSet(Object context, String expression) {
243                try {
244                        NodeList nodeList = (NodeList)getXPathFactory().newXPath().evaluate(expression, context, XPathConstants.NODESET);
245                        List<Node> nodes = new ArrayList<Node>(nodeList.getLength());
246                        for (int i = 0; i < nodeList.getLength(); i++)
247                                nodes.add(nodeList.item(i));
248                        return nodes;
249                } catch (Exception e) {
250                        throw new RuntimeException(e);
251                }
252        }
253        
254        
255        private DocumentBuilderFactory getDocumentBuilderFactory() {
256                if (documentBuilderFactory == null) {
257                        try {
258                                documentBuilderFactory = DocumentBuilderFactory.newInstance();
259        
260                                documentBuilderFactory.setCoalescing(true);
261                                documentBuilderFactory.setIgnoringComments(true);
262                        } 
263                        catch (Exception e) {
264                                throw new RuntimeException(e);
265                        }
266                }
267                return documentBuilderFactory;
268        }
269
270        private DocumentBuilderFactory getValidatingDocumentBuilderFactory() {
271                if (validatingDocumentBuilderFactory == null) {
272                        try {
273                                validatingDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
274                                validatingDocumentBuilderFactory.setCoalescing(true);
275                                validatingDocumentBuilderFactory.setIgnoringComments(true);
276                                validatingDocumentBuilderFactory.setValidating(true);
277                                validatingDocumentBuilderFactory.setIgnoringElementContentWhitespace(true);
278                        } catch (Exception e) {
279                                throw new RuntimeException(e);
280                        }
281                }
282                return validatingDocumentBuilderFactory;
283        }
284
285    private TransformerFactory getTransformerFactory() {
286        if (transformerFactory == null)
287            transformerFactory = TransformerFactory.newInstance();
288        return transformerFactory;
289    }
290    
291        private XPathFactory getXPathFactory() {
292                if (xPathFactory == null) {
293                        try {
294                                xPathFactory = XPathFactory.newInstance();
295                        }
296                        catch (Exception e) {
297                                try {
298                                        // Fallback to xalan for Google App Engine
299                                        Class<?> factoryClass = Thread.currentThread().getContextClassLoader().loadClass("org.apache.xpath.jaxp.XPathFactoryImpl");
300                                        Method m = factoryClass.getMethod("newInstance", String.class, String.class, ClassLoader.class);
301                                        xPathFactory = (XPathFactory)m.invoke(null, XPathFactory.DEFAULT_OBJECT_MODEL_URI, "org.apache.xpath.jaxp.XPathFactoryImpl", null);
302                                }
303                                catch (Exception f) {
304                                        throw new RuntimeException("XPathFactory could not be found", f);
305                                }
306                        }
307                }
308                return xPathFactory;
309        }
310
311}