/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.errai.forge.xml; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import java.io.File; import java.io.IOException; import java.util.*; public class XmlParserImpl implements AutoCloseable, XmlParser { private final File xmlFile; private final DocumentBuilder docBuilder; private final Transformer transformer; private final Map<Element, Map<String, String>> openAttributeMaps = new HashMap<Element, Map<String,String>>(); private Document document; private boolean modified; public XmlParserImpl(final File xmlFile, final Properties xmlProperties, final DocumentBuilder docBuilder, final Transformer transformer) { this.xmlFile = xmlFile; this.docBuilder = docBuilder; this.transformer = transformer; transformer.setOutputProperties(xmlProperties); } /* (non-Javadoc) * @see org.jboss.errai.forge.xml.XmlParser#open() */ @Override public void open() throws SAXException, IOException { if (!isOpen()) { document = docBuilder.parse(xmlFile); } else { throw new IllegalStateException("This instance has already been opened."); } } /* (non-Javadoc) * @see org.jboss.errai.forge.xml.XmlParser#close() */ @Override public void close() throws TransformerException { if (isOpen()) { if (modified) flush(); document = null; openAttributeMaps.clear(); } } /* (non-Javadoc) * @see org.jboss.errai.forge.xml.XmlParser#flush() */ @Override public void flush() throws TransformerException { assertOpen(); transformer.transform(new DOMSource(document), new StreamResult(xmlFile)); modified = false; } private void assertOpen() { if (!isOpen()) { throw new IllegalStateException("Document must be open for this operation."); } } /* (non-Javadoc) * @see org.jboss.errai.forge.xml.XmlParser#isOpen() */ @Override public boolean isOpen() { return document != null; } /* (non-Javadoc) * @see org.jboss.errai.forge.xml.XmlParser#addChildNodesByXPath(javax.xml.xpath.XPathExpression, java.util.Collection) */ @Override public boolean addChildNodes(final XPathExpression expression, final Collection<Node> nodes) throws XPathExpressionException { assertOpen(); final Node node = getNodeByXPath(expression); if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { for (final Node newNode : nodes) { node.appendChild(newNode); } return (modified = true); } return false; } /* (non-Javadoc) * @see org.jboss.errai.forge.xml.XmlParser#replaceNodesByXPath(javax.xml.xpath.XPathExpression, org.w3c.dom.Node) */ @Override public boolean replaceNode(final XPathExpression expression, final Node replacement) throws XPathExpressionException { assertOpen(); final Node node = getNodeByXPath(expression); if (node != null) { final Node parent = node.getParentNode(); parent.replaceChild(replacement, node); return (modified = true); } return false; } @Override public Element createElement(final String tagName) { assertOpen(); return document.createElement(tagName); } @Override public Element importElement(final Element element, boolean deep) { return (Element) document.importNode(element, deep); } private Node getNodeByXPath(XPathExpression expression) throws XPathExpressionException { return (Node) expression.evaluate(document, XPathConstants.NODE); } private boolean hasMatchingChild(Node parent, Node child) { return getMatchingChild(parent, child) != null; } /** * Find a matching child {@link Node} from a given parent node. * * @param parent * The parent node of the children to be searched. * @param inserted * The node to be matched against. * @return Returns a node, {@code result}, such that * {@code result.getParentNode().isSameNode(parent) && matches(inserted, result)} * . */ private Node getMatchingChild(Node parent, Node child) { for (int i = 0; i < parent.getChildNodes().getLength(); i++) { if (matches(child, parent.getChildNodes().item(i))) return parent.getChildNodes().item(i); } return null; } /** * Check if a node is consistent with another. A node, {@code other} is * consistent with another node, {@code node}, if the tree rooted at * {@code node} is a subtree of {@code other} (i.e. every child element, * attribute, or text value in {@code node} exists in the same relative path * in {@code other}). * * @param node * The primary node for matching against. * @param other * The secondary node being matched against the primary node. * @return True iff {@code other} is consistent with {@code node}. */ private boolean matches(Node node, Node other) { if (node.getNodeType() == Node.TEXT_NODE) { return other.getNodeType() == Node.TEXT_NODE && node.getNodeValue().equals(other.getNodeValue()); } if (!(other instanceof Element) || !(node instanceof Element)) { return false; } final Element e1 = (Element) node, e2 = (Element) other; if (!e1.getNodeName().equals(e2.getNodeName())) return false; // other must have attributes consistent with node final NamedNodeMap attributes = e1.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { final Node item = attributes.item(i); if (!e2.hasAttribute(item.getNodeName()) || !e2.getAttribute(item.getNodeName()).equals(item.getNodeValue())) return false; } // children of other must be consistent with children of node if (e1.hasChildNodes()) { outer: for (Node child = e1.getFirstChild(); child != null; child = child.getNextSibling()) { if (child.getNodeType() == Node.ELEMENT_NODE || child.getNodeType() == Node.TEXT_NODE) { for (Node otherChild = e2.getFirstChild(); otherChild != null; otherChild = otherChild.getNextSibling()) { if (otherChild.getNodeType() == child.getNodeType() && matches(child, otherChild)) continue outer; } } else { continue; } return false; } } return true; } @Override public boolean hasMatchingChild(final XPathExpression parentExpression, final Node child) throws XPathExpressionException { assertOpen(); final Node parentNode = getNodeByXPath(parentExpression); return parentNode != null && hasMatchingChild(parentNode, child); } @Override public boolean hasNode(final XPathExpression expression) throws XPathExpressionException { assertOpen(); return getNodeByXPath(expression) != null; } @Override public boolean matches(final XPathExpression expression, final Node node) throws XPathExpressionException { assertOpen(); final Node documentNode = getNodeByXPath(expression); return documentNode != null && matches(node, documentNode); } @Override public boolean removeNode(final XPathExpression expression) throws XPathExpressionException { assertOpen(); final Node removableNode = getNodeByXPath(expression); if (removableNode == null) return false; removableNode.getParentNode().removeChild(removableNode); return (modified = true); } @Override public boolean removeChildNode(final XPathExpression parentExpression, final Node child) throws XPathExpressionException { assertOpen(); final Node parent = getNodeByXPath(parentExpression); if (parent == null) return false; final Node matchingChild = getMatchingChild(parent, child); if (matchingChild == null) return false; parent.removeChild(matchingChild); return (modified = true); } @Override public Map<String, String> getAttributes(final XPathExpression elementExpression) throws XPathExpressionException { final Element element = (Element) getNodeByXPath(elementExpression); if (element != null) { final Map<String, String> retVal = new Map<String, String>() { @Override public String put(final String key, final String value) { assertOpen(); modified = true; final String prev = element.getAttribute(key); element.setAttribute(key, value); return (!prev.equals("")) ? prev : null; } @Override public void putAll(final Map<? extends String, ? extends String> m) { assertOpen(); modified = true; for (final Entry<? extends String, ? extends String> pair : m.entrySet()) { element.setAttribute(pair.getKey(), pair.getValue()); } } @Override public String remove(Object key) { assertOpen(); modified = true; final String prev = element.getAttribute((String) key); element.removeAttribute((String) key); return (!prev.equals("")) ? prev : null; } @Override public void clear() { throw new UnsupportedOperationException(); } @Override public int size() { throw new UnsupportedOperationException(); } @Override public boolean isEmpty() { throw new UnsupportedOperationException(); } @Override public boolean containsKey(Object key) { return element.hasAttribute((String) key); } @Override public boolean containsValue(Object value) { throw new UnsupportedOperationException(); } @Override public String get(Object key) { return element.getAttribute((String) key); } @Override public Set<String> keySet() { throw new UnsupportedOperationException(); } @Override public Collection<String> values() { throw new UnsupportedOperationException(); } @Override public Set<java.util.Map.Entry<String, String>> entrySet() { throw new UnsupportedOperationException(); } }; return retVal; } return null; } }