Coverage Report - com.jcabi.xml.XMLDocument
 
Classes in this File Line Coverage Branch Coverage Complexity
XMLDocument
83%
89/107
58%
21/36
2.444
 
 1  
 /**
 2  
  * Copyright (c) 2012-2017, jcabi.com
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions
 7  
  * are met: 1) Redistributions of source code must retain the above
 8  
  * copyright notice, this list of conditions and the following
 9  
  * disclaimer. 2) Redistributions in binary form must reproduce the above
 10  
  * copyright notice, this list of conditions and the following
 11  
  * disclaimer in the documentation and/or other materials provided
 12  
  * with the distribution. 3) Neither the name of the jcabi.com nor
 13  
  * the names of its contributors may be used to endorse or promote
 14  
  * products derived from this software without specific prior written
 15  
  * permission.
 16  
  *
 17  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18  
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 19  
  * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 20  
  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 21  
  * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 22  
  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 24  
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 25  
  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 26  
  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 27  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 28  
  * OF THE POSSIBILITY OF SUCH DAMAGE.
 29  
  */
 30  
 package com.jcabi.xml;
 31  
 
 32  
 import java.io.File;
 33  
 import java.io.IOException;
 34  
 import java.io.InputStream;
 35  
 import java.io.StringWriter;
 36  
 import java.net.URI;
 37  
 import java.net.URL;
 38  
 import java.util.ArrayList;
 39  
 import java.util.Collections;
 40  
 import java.util.List;
 41  
 import javax.xml.namespace.NamespaceContext;
 42  
 import javax.xml.namespace.QName;
 43  
 import javax.xml.parsers.DocumentBuilderFactory;
 44  
 import javax.xml.parsers.ParserConfigurationException;
 45  
 import javax.xml.transform.OutputKeys;
 46  
 import javax.xml.transform.Source;
 47  
 import javax.xml.transform.Transformer;
 48  
 import javax.xml.transform.TransformerConfigurationException;
 49  
 import javax.xml.transform.TransformerException;
 50  
 import javax.xml.transform.TransformerFactory;
 51  
 import javax.xml.transform.dom.DOMResult;
 52  
 import javax.xml.transform.dom.DOMSource;
 53  
 import javax.xml.transform.stream.StreamResult;
 54  
 import javax.xml.xpath.XPath;
 55  
 import javax.xml.xpath.XPathConstants;
 56  
 import javax.xml.xpath.XPathExpressionException;
 57  
 import javax.xml.xpath.XPathFactory;
 58  
 import lombok.EqualsAndHashCode;
 59  
 import org.w3c.dom.Document;
 60  
 import org.w3c.dom.Node;
 61  
 import org.w3c.dom.NodeList;
 62  
 
 63  
 /**
 64  
  * Implementation of {@link XML}.
 65  
  *
 66  
  * <p>Objects of this class are immutable and thread-safe.
 67  
  *
 68  
  * @author Yegor Bugayenko (yegor@teamed.io)
 69  
  * @version $Id: d54bc5c86d5968a13c7f2b389fc2eea9d6e555a8 $
 70  
  * @since 0.1
 71  
  * @checkstyle ClassDataAbstractionCoupling (500 lines)
 72  
  * @checkstyle ClassFanOutComplexity (500 lines)
 73  
  */
 74  2
 @EqualsAndHashCode(of = { "xml", "leaf" })
 75  
 @SuppressWarnings("PMD.ExcessiveImports")
 76  
 public final class XMLDocument implements XML {
 77  
 
 78  
     /**
 79  
      * XPath factory.
 80  
      */
 81  1
     private static final XPathFactory XFACTORY =
 82  
         XPathFactory.newInstance();
 83  
 
 84  
     /**
 85  
      * Transformer factory.
 86  
      */
 87  1
     private static final TransformerFactory TFACTORY =
 88  
         TransformerFactory.newInstance();
 89  
 
 90  
     /**
 91  
      * DOM document builder factory.
 92  
      */
 93  1
     private static final DocumentBuilderFactory DFACTORY =
 94  
         DocumentBuilderFactory.newInstance();
 95  
 
 96  
     /**
 97  
      * Namespace context to use for {@link #xpath(String)}
 98  
      * and {@link #nodes(String)} methods.
 99  
      */
 100  
     private final transient XPathContext context;
 101  
 
 102  
     /**
 103  
      * Encapsulated String representation of this XML document.
 104  
      */
 105  
     private final transient String xml;
 106  
 
 107  
     /**
 108  
      * Is it a leaf node (Element, not a Document)?
 109  
      */
 110  
     private final transient boolean leaf;
 111  
 
 112  
     /**
 113  
      * Actual XML document node. Needs to be an Object so the class is still
 114  
      * recognized as @Immutable.
 115  
      */
 116  
     private final transient Object cache;
 117  
 
 118  
     static {
 119  1
         if (XMLDocument.DFACTORY.getClass().getName().contains("xerces")) {
 120  
             try {
 121  1
                 XMLDocument.DFACTORY.setFeature(
 122  
                     // @checkstyle LineLength (1 line)
 123  
                     "http://apache.org/xml/features/nonvalidating/load-external-dtd",
 124  
                     false
 125  
                 );
 126  0
             } catch (final ParserConfigurationException ex) {
 127  0
                 throw new IllegalStateException(ex);
 128  1
             }
 129  
         }
 130  1
         XMLDocument.DFACTORY.setNamespaceAware(true);
 131  1
     }
 132  
 
 133  
     /**
 134  
      * Public ctor, from XML as a text.
 135  
      *
 136  
      * <p>The object is created with a default implementation of
 137  
      * {@link NamespaceContext}, which already defines a
 138  
      * number of namespaces, for convenience, including:
 139  
      *
 140  
      * <pre> xhtml: http://www.w3.org/1999/xhtml
 141  
      * xs: http://www.w3.org/2001/XMLSchema
 142  
      * xsi: http://www.w3.org/2001/XMLSchema-instance
 143  
      * xsl: http://www.w3.org/1999/XSL/Transform
 144  
      * svg: http://www.w3.org/2000/svg</pre>
 145  
      *
 146  
      * <p>In future versions we will add more namespaces (submit a ticket if
 147  
      * you need more of them defined here).
 148  
      *
 149  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 150  
      * passed is not in XML format.
 151  
      *
 152  
      * @param text XML document body
 153  
      */
 154  
     public XMLDocument(final String text) {
 155  408
         this(
 156  
             new DomParser(XMLDocument.DFACTORY, text).document(),
 157  
             new XPathContext(),
 158  
             false
 159  
         );
 160  408
     }
 161  
 
 162  
     /**
 163  
      * Public ctor, from a DOM node.
 164  
      *
 165  
      * <p>The object is created with a default implementation of
 166  
      * {@link NamespaceContext}, which already defines a
 167  
      * number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
 168  
      *
 169  
      * @param node DOM source
 170  
      * @since 0.2
 171  
      */
 172  
     public XMLDocument(final Node node) {
 173  57
         this(node, new XPathContext(), !(node instanceof Document));
 174  57
     }
 175  
 
 176  
     /**
 177  
      * Public ctor, from a source.
 178  
      *
 179  
      * <p>The object is created with a default implementation of
 180  
      * {@link NamespaceContext}, which already defines a
 181  
      * number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
 182  
      *
 183  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 184  
      * passed is not in XML format.
 185  
      *
 186  
      * @param source Source of XML document
 187  
      */
 188  
     public XMLDocument(final Source source) {
 189  209
         this(XMLDocument.transform(source), new XPathContext(), false);
 190  209
     }
 191  
 
 192  
     /**
 193  
      * Public ctor, from XML in a file.
 194  
      *
 195  
      * <p>The object is created with a default implementation of
 196  
      * {@link NamespaceContext}, which already defines a
 197  
      * number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
 198  
      *
 199  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 200  
      * passed is not in XML format.
 201  
      *
 202  
      * @param file XML file
 203  
      * @throws IOException In case of I/O problems
 204  
      */
 205  
     public XMLDocument(final File file) throws IOException {
 206  1
         this(new TextResource(file).toString());
 207  1
     }
 208  
 
 209  
     /**
 210  
      * Public ctor, from XML in the URL.
 211  
      *
 212  
      * <p>The object is created with a default implementation of
 213  
      * {@link NamespaceContext}, which already defines a
 214  
      * number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
 215  
      *
 216  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 217  
      * passed is not in XML format.
 218  
      *
 219  
      * @param url The URL to load from
 220  
      * @throws IOException In case of I/O problems
 221  
      */
 222  
     public XMLDocument(final URL url) throws IOException {
 223  3
         this(new TextResource(url).toString());
 224  3
     }
 225  
 
 226  
     /**
 227  
      * Public ctor, from XML in the URI.
 228  
      *
 229  
      * <p>The object is created with a default implementation of
 230  
      * {@link NamespaceContext}, which already defines a
 231  
      * number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
 232  
      *
 233  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 234  
      * passed is not in XML format.
 235  
      *
 236  
      * @param uri The URI to load from
 237  
      * @throws IOException In case of I/O problems
 238  
      */
 239  
     public XMLDocument(final URI uri) throws IOException {
 240  0
         this(new TextResource(uri).toString());
 241  0
     }
 242  
 
 243  
     /**
 244  
      * Public ctor, from input stream.
 245  
      *
 246  
      * <p>The object is created with a default implementation of
 247  
      * {@link NamespaceContext}, which already defines a
 248  
      * number of namespaces, see {@link XMLDocument#XMLDocument(String)}.
 249  
      *
 250  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 251  
      * passed is not in XML format.
 252  
      *
 253  
      * <p>The provided input stream will be closed automatically after
 254  
      * getting data from it.
 255  
      *
 256  
      * @param stream The input stream, which will be closed automatically
 257  
      * @throws IOException In case of I/O problem
 258  
      */
 259  
     public XMLDocument(final InputStream stream) throws IOException {
 260  1
         this(new TextResource(stream).toString());
 261  1
         stream.close();
 262  1
     }
 263  
 
 264  
     /**
 265  
      * Private ctor.
 266  
      * @param node The source
 267  
      * @param ctx Namespace context
 268  
      * @param lfe Is it a leaf node?
 269  
      */
 270  
     private XMLDocument(final Node node, final XPathContext ctx,
 271  51151
         final boolean lfe) {
 272  51149
         this.xml = XMLDocument.asString(node);
 273  51152
         this.context = ctx;
 274  51152
         this.leaf = lfe;
 275  51152
         this.cache = node;
 276  51152
     }
 277  
 
 278  
     @Override
 279  
     public String toString() {
 280  263
         return this.xml;
 281  
     }
 282  
 
 283  
     @Override
 284  
     public Node node() {
 285  1312
         final Node castCache = Node.class.cast(this.cache);
 286  
         final Node answer;
 287  1310
         if (castCache instanceof Document) {
 288  1202
             answer = castCache.cloneNode(true);
 289  
         } else {
 290  108
             answer = this.createImportedNode(castCache);
 291  
         }
 292  1312
         return answer;
 293  
     }
 294  
 
 295  
     @Override
 296  
     @SuppressWarnings
 297  
         (
 298  
             {
 299  
                 "PMD.ExceptionAsFlowControl",
 300  
                 "PMD.PreserveStackTrace"
 301  
             }
 302  
         )
 303  
     public List<String> xpath(final String query) {
 304  
         List<String> items;
 305  
         try {
 306  59
             final NodeList nodes = this.fetch(query, NodeList.class);
 307  56
             items = new ArrayList<String>(nodes.getLength());
 308  113
             for (int idx = 0; idx < nodes.getLength(); ++idx) {
 309  57
                 final int type = nodes.item(idx).getNodeType();
 310  57
                 if (type != Node.TEXT_NODE && type != Node.ATTRIBUTE_NODE
 311  
                     && type != Node.CDATA_SECTION_NODE) {
 312  0
                     throw new IllegalArgumentException(
 313  
                         String.format(
 314  
                             // @checkstyle LineLength (1 line)
 315  
                             "Only text() nodes or attributes are retrievable with xpath() '%s': %d",
 316  
                             query, type
 317  
                         )
 318  
                     );
 319  
                 }
 320  57
                 items.add(nodes.item(idx).getNodeValue());
 321  
             }
 322  3
         } catch (final XPathExpressionException ex) {
 323  
             try {
 324  3
                 items = Collections.singletonList(
 325  
                     this.fetch(query, String.class)
 326  
                 );
 327  1
             } catch (final XPathExpressionException exp) {
 328  1
                 throw new IllegalArgumentException(
 329  
                     // @checkstyle MultipleStringLiterals (1 line)
 330  
                     String.format(
 331  
                         "invalid XPath query '%s' at %s",
 332  
                         query, XMLDocument.XFACTORY.getClass().getName()
 333  
                     ), exp
 334  
                 );
 335  2
             }
 336  56
         }
 337  58
         return new ListWrapper<String>(items, this.node(), query);
 338  
     }
 339  
 
 340  
     @Override
 341  
     public XML registerNs(final String prefix, final Object uri) {
 342  1
         return new XMLDocument(
 343  
             this.node(), this.context.add(prefix, uri), this.leaf
 344  
         );
 345  
     }
 346  
 
 347  
     @Override
 348  
     @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
 349  
     public List<XML> nodes(final String query) {
 350  
         final List<XML> items;
 351  
         try {
 352  320
             final NodeList nodes = this.fetch(query, NodeList.class);
 353  319
             items = new ArrayList<XML>(nodes.getLength());
 354  50585
             for (int idx = 0; idx < nodes.getLength(); ++idx) {
 355  50267
                 items.add(
 356  
                     new XMLDocument(
 357  
                         nodes.item(idx),
 358  
                         this.context, true
 359  
                     )
 360  
                 );
 361  
             }
 362  0
         } catch (final XPathExpressionException ex) {
 363  0
             throw new IllegalArgumentException(
 364  
                 String.format(
 365  
                     "invalid XPath query '%s' by %s",
 366  
                     query, XMLDocument.XFACTORY.getClass().getName()
 367  
                 ), ex
 368  
             );
 369  320
         }
 370  320
         return new ListWrapper<XML>(items, this.node(), query);
 371  
     }
 372  
 
 373  
     @Override
 374  
     public XML merge(final NamespaceContext ctx) {
 375  209
         return new XMLDocument(this.node(), this.context.merge(ctx), this.leaf);
 376  
     }
 377  
 
 378  
     /**
 379  
      * Clones a node and imports it in a new document.
 380  
      * @param node A node to clone.
 381  
      * @return A cloned node imported in a dedicated document.
 382  
      */
 383  
     private Node createImportedNode(final Node node) {
 384  
         final Document document;
 385  
         try {
 386  108
             document = DFACTORY.newDocumentBuilder().newDocument();
 387  0
         } catch (final ParserConfigurationException ex) {
 388  0
             throw new IllegalStateException(ex);
 389  108
         }
 390  108
         final Node imported = document.importNode(node, true);
 391  108
         document.appendChild(imported);
 392  108
         return imported;
 393  
     }
 394  
 
 395  
     /**
 396  
      * Retrieve XPath query result. Supports returning {@link NodeList} and
 397  
      * {@link String} types.
 398  
      *
 399  
      * <p>An {@link IllegalArgumentException} is thrown if the parameter
 400  
      * passed is not a valid XPath expression or an unsupported type is
 401  
      * specified.
 402  
      *
 403  
      * @param <T> The type to return
 404  
      * @param query XPath query
 405  
      * @param type The return type
 406  
      * @return Result of XPath query
 407  
      * @throws XPathExpressionException If an error occurs when evaluating XPath
 408  
      */
 409  
     @SuppressWarnings("unchecked")
 410  
     private <T> T fetch(final String query, final Class<T> type)
 411  
         throws XPathExpressionException {
 412  
         final XPath xpath;
 413  382
         synchronized (XMLDocument.class) {
 414  382
             xpath = XMLDocument.XFACTORY.newXPath();
 415  382
         }
 416  382
         xpath.setNamespaceContext(this.context);
 417  
         final QName qname;
 418  382
         if (type.equals(String.class)) {
 419  3
             qname = XPathConstants.STRING;
 420  379
         } else if (type.equals(NodeList.class)) {
 421  379
             qname = XPathConstants.NODESET;
 422  
         } else {
 423  0
             throw new IllegalArgumentException(
 424  
                 String.format(
 425  
                     "Unsupported type: %s", type.getName()
 426  
                 )
 427  
             );
 428  
         }
 429  382
         return (T) xpath.evaluate(query, this.node(), qname);
 430  
     }
 431  
 
 432  
     /**
 433  
      * Transform node to String.
 434  
      *
 435  
      * @param node The DOM node.
 436  
      * @return String representation
 437  
      */
 438  
     private static String asString(final Node node) {
 439  51151
         final StringWriter writer = new StringWriter();
 440  
         try {
 441  
             final Transformer trans;
 442  51147
             synchronized (XMLDocument.class) {
 443  51156
                 trans = XMLDocument.TFACTORY.newTransformer();
 444  51156
             }
 445  
             // @checkstyle MultipleStringLiterals (1 line)
 446  51156
             trans.setOutputProperty(OutputKeys.INDENT, "yes");
 447  51156
             trans.setOutputProperty(OutputKeys.VERSION, "1.0");
 448  51156
             if (!(node instanceof Document)) {
 449  50272
                 trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
 450  
             }
 451  51156
             synchronized (node) {
 452  51156
                 trans.transform(
 453  
                     new DOMSource(node),
 454  
                     new StreamResult(writer)
 455  
                 );
 456  51149
             }
 457  0
         } catch (final TransformerConfigurationException ex) {
 458  0
             throw new IllegalStateException(ex);
 459  0
         } catch (final TransformerException ex) {
 460  0
             throw new IllegalArgumentException(ex);
 461  51152
         }
 462  51153
         return writer.toString();
 463  
     }
 464  
 
 465  
     /**
 466  
      * Transform source to DOM node.
 467  
      * @param source The source
 468  
      * @return The node
 469  
      */
 470  
     private static Node transform(final Source source) {
 471  209
         final DOMResult result = new DOMResult();
 472  
         try {
 473  
             final Transformer trans;
 474  209
             synchronized (XMLDocument.class) {
 475  209
                 trans = XMLDocument.TFACTORY.newTransformer();
 476  209
             }
 477  209
             trans.transform(source, result);
 478  0
         } catch (final TransformerConfigurationException ex) {
 479  0
             throw new IllegalStateException(ex);
 480  0
         } catch (final TransformerException ex) {
 481  0
             throw new IllegalStateException(ex);
 482  209
         }
 483  209
         return result.getNode();
 484  
     }
 485  
 
 486  
 }