All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jcabi.xml.XMLDocument Maven / Gradle / Ivy

There is a newer version: 0.30.1
Show newest version
/**
 * Copyright (c) 2012-2017, jcabi.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following
 * disclaimer. 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the jcabi.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jcabi.xml;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.EqualsAndHashCode;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Implementation of {@link XML}.
 *
 * 

Objects of this class are immutable and thread-safe. * * @author Yegor Bugayenko ([email protected]) * @version $Id: 4292fcb6a02749a125998672abc86bb7c1335e67 $ * @since 0.1 * @checkstyle ClassDataAbstractionCoupling (500 lines) * @checkstyle ClassFanOutComplexity (500 lines) * @checkstyle AbbreviationAsWordInNameCheck (10 lines) */ @EqualsAndHashCode(of = { "xml", "leaf" }) @SuppressWarnings ( { "PMD.ExcessiveImports", "PMD.OnlyOneConstructorShouldDoInitialization" } ) public final class XMLDocument implements XML { /** * XPath factory. */ private static final XPathFactory XFACTORY = XPathFactory.newInstance(); /** * Transformer factory. */ private static final TransformerFactory TFACTORY = TransformerFactory.newInstance(); /** * DOM document builder factory. */ private static final DocumentBuilderFactory DFACTORY = DocumentBuilderFactory.newInstance(); /** * Namespace context to use for {@link #xpath(String)} * and {@link #nodes(String)} methods. */ private final transient XPathContext context; /** * Encapsulated String representation of this XML document. */ private final transient String xml; /** * Is it a leaf node (Element, not a Document)? */ private final transient boolean leaf; /** * Actual XML document node. Needs to be an Object so the class is still * recognized as @Immutable. */ private final transient Object cache; static { if (XMLDocument.DFACTORY.getClass().getName().contains("xerces")) { try { XMLDocument.DFACTORY.setFeature( // @checkstyle LineLength (1 line) "http://apache.org/xml/features/nonvalidating/load-external-dtd", false ); } catch (final ParserConfigurationException ex) { throw new IllegalStateException(ex); } } XMLDocument.DFACTORY.setNamespaceAware(true); } /** * Public ctor, from XML as a text. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, for convenience, including: * *

 xhtml: http://www.w3.org/1999/xhtml
     * xs: http://www.w3.org/2001/XMLSchema
     * xsi: http://www.w3.org/2001/XMLSchema-instance
     * xsl: http://www.w3.org/1999/XSL/Transform
     * svg: http://www.w3.org/2000/svg
* *

In future versions we will add more namespaces (submit a ticket if * you need more of them defined here). * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * * @param text XML document body */ public XMLDocument(final String text) { this( new DomParser(XMLDocument.DFACTORY, text).document(), new XPathContext(), false ); } /** * Public ctor, from a DOM node. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * * @param node DOM source * @since 0.2 */ public XMLDocument(final Node node) { this(node, new XPathContext(), !(node instanceof Document)); } /** * Public ctor, from a source. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * * @param source Source of XML document */ public XMLDocument(final Source source) { this(XMLDocument.transform(source), new XPathContext(), false); } /** * Public ctor, from XML in a file. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * * @param file XML file * @throws FileNotFoundException In case of I/O problems */ public XMLDocument(final File file) throws FileNotFoundException { this(new TextResource(file).toString()); } /** * Public ctor, from XML in a file. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * * @param file XML file * @throws FileNotFoundException In case of I/O problems */ public XMLDocument(final Path file) throws FileNotFoundException { this(file.toFile()); } /** * Public ctor, from XML in the URL. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * * @param url The URL to load from * @throws IOException In case of I/O problems */ public XMLDocument(final URL url) throws IOException { this(new TextResource(url).toString()); } /** * Public ctor, from XML in the URI. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * * @param uri The URI to load from * @throws IOException In case of I/O problems */ public XMLDocument(final URI uri) throws IOException { this(new TextResource(uri).toString()); } /** * Public ctor, from input stream. * *

The object is created with a default implementation of * {@link NamespaceContext}, which already defines a * number of namespaces, see {@link XMLDocument#XMLDocument(String)}. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not in XML format. * *

The provided input stream will be closed automatically after * getting data from it. * * @param stream The input stream, which will be closed automatically * @throws IOException In case of I/O problem */ @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors") public XMLDocument(final InputStream stream) throws IOException { this(new TextResource(stream).toString()); stream.close(); } /** * Private ctor. * @param node The source * @param ctx Namespace context * @param lfe Is it a leaf node? */ private XMLDocument(final Node node, final XPathContext ctx, final boolean lfe) { this.xml = XMLDocument.asString(node); this.context = ctx; this.leaf = lfe; this.cache = node; } @Override public String toString() { return this.xml; } @Override public Node node() { final Node casted = Node.class.cast(this.cache); final Node answer; if (casted instanceof Document) { answer = casted.cloneNode(true); } else { answer = XMLDocument.createImportedNode(casted); } return answer; } @Override @SuppressWarnings ( { "PMD.ExceptionAsFlowControl", "PMD.PreserveStackTrace" } ) public List xpath(final String query) { // @checkstyle FinalLocalVariableCheck (1 line) List items; try { final NodeList nodes = this.fetch(query, NodeList.class); items = new ArrayList<>(nodes.getLength()); for (int idx = 0; idx < nodes.getLength(); ++idx) { final int type = (int) nodes.item(idx).getNodeType(); if (type != (int) Node.TEXT_NODE && type != (int) Node.ATTRIBUTE_NODE && type != (int) Node.CDATA_SECTION_NODE) { throw new IllegalArgumentException( String.format( // @checkstyle LineLength (1 line) "Only text() nodes or attributes are retrievable with xpath() '%s': %d", query, type ) ); } items.add(nodes.item(idx).getNodeValue()); } } catch (final XPathExpressionException ex) { try { items = Collections.singletonList( this.fetch(query, String.class) ); } catch (final XPathExpressionException exp) { throw new IllegalArgumentException( // @checkstyle MultipleStringLiterals (1 line) String.format( "Invalid XPath query '%s' at %s: %s", query, XMLDocument.XFACTORY.getClass().getName(), ex.getLocalizedMessage() ), exp ); } } return new ListWrapper<>(items, Node.class.cast(this.cache), query); } @Override public XML registerNs(final String prefix, final Object uri) { return new XMLDocument( Node.class.cast(this.cache), this.context.add(prefix, uri), this.leaf ); } @Override @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") public List nodes(final String query) { final List items; try { final NodeList nodes = this.fetch(query, NodeList.class); items = new ArrayList<>(nodes.getLength()); for (int idx = 0; idx < nodes.getLength(); ++idx) { items.add( new XMLDocument( nodes.item(idx), this.context, true ) ); } } catch (final XPathExpressionException ex) { throw new IllegalArgumentException( String.format( "invalid XPath query '%s' by %s", query, XMLDocument.XFACTORY.getClass().getName() ), ex ); } return new ListWrapper<>(items, Node.class.cast(this.cache), query); } @Override public XML merge(final NamespaceContext ctx) { return new XMLDocument( Node.class.cast(this.cache), this.context.merge(ctx), this.leaf ); } /** * Clones a node and imports it in a new document. * @param node A node to clone. * @return A cloned node imported in a dedicated document. */ private static Node createImportedNode(final Node node) { final Document document; try { document = XMLDocument.DFACTORY.newDocumentBuilder().newDocument(); } catch (final ParserConfigurationException ex) { throw new IllegalStateException(ex); } final Node imported = document.importNode(node, true); document.appendChild(imported); return imported; } /** * Retrieve XPath query result. Supports returning {@link NodeList} and * {@link String} types. * *

An {@link IllegalArgumentException} is thrown if the parameter * passed is not a valid XPath expression or an unsupported type is * specified. * * @param The type to return * @param query XPath query * @param type The return type * @return Result of XPath query * @throws XPathExpressionException If an error occurs when evaluating XPath */ @SuppressWarnings("unchecked") private T fetch(final String query, final Class type) throws XPathExpressionException { final XPath xpath; synchronized (XMLDocument.class) { xpath = XMLDocument.XFACTORY.newXPath(); } xpath.setNamespaceContext(this.context); final QName qname; if (type.equals(String.class)) { qname = XPathConstants.STRING; } else if (type.equals(NodeList.class)) { qname = XPathConstants.NODESET; } else { throw new IllegalArgumentException( String.format( "Unsupported type: %s", type.getName() ) ); } return (T) xpath.evaluate(query, Node.class.cast(this.cache), qname); } /** * Transform node to String. * * @param node The DOM node. * @return String representation */ @SuppressWarnings("PMD.AvoidDuplicateLiterals") private static String asString(final Node node) { final StringWriter writer = new StringWriter(); try { final Transformer trans; synchronized (XMLDocument.class) { trans = XMLDocument.TFACTORY.newTransformer(); } // @checkstyle MultipleStringLiterals (1 line) trans.setOutputProperty(OutputKeys.INDENT, "yes"); trans.setOutputProperty(OutputKeys.VERSION, "1.0"); if (!(node instanceof Document)) { trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); } synchronized (node) { trans.transform( new DOMSource(node), new StreamResult(writer) ); } } catch (final TransformerConfigurationException ex) { throw new IllegalStateException(ex); } catch (final TransformerException ex) { throw new IllegalArgumentException(ex); } return writer.toString(); } /** * Transform source to DOM node. * @param source The source * @return The node */ private static Node transform(final Source source) { final DOMResult result = new DOMResult(); try { final Transformer trans; synchronized (XMLDocument.class) { trans = XMLDocument.TFACTORY.newTransformer(); } trans.transform(source, result); } catch (final TransformerException ex) { throw new IllegalStateException(ex); } return result.getNode(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy