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