/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to You 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.apache.geode.management.internal.configuration.utils; import static org.apache.geode.management.internal.configuration.utils.XmlConstants.W3C_XML_SCHEMA_INSTANCE_ATTRIBUTE_SCHEMA_LOCATION; import static org.apache.geode.management.internal.configuration.utils.XmlConstants.W3C_XML_SCHEMA_INSTANCE_PREFIX; import static javax.xml.XMLConstants.NULL_NS_URI; import static javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI; import java.io.File; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.StringTokenizer; import javax.xml.namespace.NamespaceContext; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; 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 org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.apache.geode.internal.cache.xmlcache.CacheXml; import org.apache.geode.internal.cache.xmlcache.CacheXmlParser; import org.apache.geode.internal.lang.StringUtils; import org.apache.geode.management.internal.configuration.domain.CacheElement; import org.apache.geode.management.internal.configuration.domain.XmlEntity; public class XmlUtils { /** * Create an XML {@link Document} from the given {@link Reader}. * * @param reader to create document from. * @return {@link Document} if successful, otherwise false. * @throws ParserConfigurationException * @throws SAXException * @throws IOException * @since GemFire 8.1 */ public static Document createDocumentFromReader(final Reader reader) throws SAXException, ParserConfigurationException, IOException { Document doc = null; InputSource inputSource = new InputSource(reader); doc = getDocumentBuilder().parse(inputSource); return doc; } public static NodeList query(Node node, String searchString) throws XPathExpressionException { XPath xpath = XPathFactory.newInstance().newXPath(); return (NodeList) xpath.evaluate(searchString, node, XPathConstants.NODESET); } public static NodeList query(Node node, String searchString, XPathContext xpathcontext) throws XPathExpressionException { XPath xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(xpathcontext); return (NodeList) xpath.evaluate(searchString, node, XPathConstants.NODESET); } public static Element querySingleElement(Node node, String searchString, final XPathContext xPathContext) throws XPathExpressionException { XPath xpath = XPathFactory.newInstance().newXPath(); xpath.setNamespaceContext(xPathContext); Object result = xpath.evaluate(searchString, node, XPathConstants.NODE); try { return (Element) result; } catch (ClassCastException e) { throw new XPathExpressionException("Not an org.w3c.dom.Element: " + result); } } public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); // the actual builder or parser DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(new CacheXmlParser()); return builder; } /***** * Adds a new node or replaces an existing node in the Document * * @param doc Target document where the node will added * @param xmlEntity contains definition of the xml entity * @throws IOException * @throws ParserConfigurationException * @throws SAXException * @throws XPathExpressionException */ public static void addNewNode(final Document doc, final XmlEntity xmlEntity) throws IOException, XPathExpressionException, SAXException, ParserConfigurationException { // Build up map per call to avoid issues with caching wrong version of the map. final LinkedHashMap<String, CacheElement> elementOrderMap = CacheElement.buildElementMap(doc); final Node newNode = createNode(doc, xmlEntity.getXmlDefinition()); final Node root = doc.getDocumentElement(); final int incomingElementOrder = getElementOrder(elementOrderMap, xmlEntity.getNamespace(), xmlEntity.getType()); boolean nodeAdded = false; NodeList nodes = root.getChildNodes(); final int length = nodes.getLength(); for (int i = 0; i < length; i++) { final Node node = nodes.item(i); if (node instanceof Element) { final Element childElement = (Element) node; final String type = childElement.getLocalName(); final String namespace = childElement.getNamespaceURI(); if (namespace.equals(xmlEntity.getNamespace()) && type.equals(xmlEntity.getType())) { // TODO this should really be checking all attributes in xmlEntity.getAttributes // First check if the element has a name String nameOrId = getAttribute(childElement, "name"); // If not then check if the element has an Id if (nameOrId == null) { nameOrId = getAttribute(childElement, "id"); } if (nameOrId != null) { // If there is a match , then replace the existing node with the incoming node if (nameOrId.equals(xmlEntity.getNameOrId())) { root.replaceChild(newNode, node); nodeAdded = true; break; } } else { // This element does not have any name or id identifier for e.g PDX and gateway-receiver // If there is only one element of that type then replace it with the incoming node if (!isMultiple(elementOrderMap, namespace, type)) { root.replaceChild(newNode, node); nodeAdded = true; break; } } } else { if (incomingElementOrder < getElementOrder(elementOrderMap, namespace, type)) { root.insertBefore(newNode, node); nodeAdded = true; break; } } } } if (!nodeAdded) { root.appendChild(newNode); } } /** * @param elementOrderMap * @param namespace * @param type * @return <code>true</code> if element allows multiple, otherwise <code>false</code>. * @since GemFire 8.1 */ private static boolean isMultiple(final LinkedHashMap<String, CacheElement> elementOrderMap, final String namespace, final String type) { if (CacheXml.GEODE_NAMESPACE.equals(namespace)) { // We only keep the cache elements in elementOrderMap final CacheElement cacheElement = elementOrderMap.get(type); if (null != cacheElement) { return cacheElement.isMultiple(); } } // Assume all extensions are not multiples. // To support multiple on extensions our map needs to included other // namespaces return false; } /** * @param elementOrderMap * @param namespace * @param type * @return position of the element if in map, otherwise {@link Integer#MAX_VALUE}. * @since GemFire 8.1 */ private static int getElementOrder(final LinkedHashMap<String, CacheElement> elementOrderMap, final String namespace, final String type) { if (CacheXml.GEODE_NAMESPACE.equals(namespace)) { // We only keep the cache elements in elementOrderMap final CacheElement cacheElement = elementOrderMap.get(type); if (null != cacheElement) { return cacheElement.getOrder(); } } // Assume all extensions are order independent. return Integer.MAX_VALUE; } /**** * Creates a node from the String xml definition * * @param owner * @param xmlDefintion * @return Node representing the xml definition * @throws ParserConfigurationException * @throws IOException * @throws SAXException */ private static Node createNode(Document owner, String xmlDefintion) throws SAXException, IOException, ParserConfigurationException { InputSource inputSource = new InputSource(new StringReader(xmlDefintion)); Document document = getDocumentBuilder().parse(inputSource); Node newNode = document.getDocumentElement(); return owner.importNode(newNode, true); } public static String getAttribute(Node node, String name) { NamedNodeMap attributes = node.getAttributes(); if (attributes == null) { return null; } Node attributeNode = node.getAttributes().getNamedItem(name); if (attributeNode == null) { return null; } return attributeNode.getTextContent(); } public static String getAttribute(Node node, String localName, String namespaceURI) { Node attributeNode = node.getAttributes().getNamedItemNS(namespaceURI, localName); if (attributeNode == null) { return null; } return attributeNode.getTextContent(); } /** * Build schema location map of schemas used in given <code>schemaLocationAttribute</code>. * * @see <a href="http://www.w3.org/TR/xmlschema-0/#schemaLocation">XML Schema Part 0: Primer * Second Edition | 5.6 schemaLocation</a> * * @param schemaLocation attribute value to build schema location map from. * @return {@link Map} of schema namespace URIs to location URLs. * @since GemFire 8.1 */ public static final Map<String, List<String>> buildSchemaLocationMap( final String schemaLocation) { return buildSchemaLocationMap(new HashMap<String, List<String>>(), schemaLocation); } /** * Build schema location map of schemas used in given <code>schemaLocationAttribute</code> and * adds them to the given <code>schemaLocationMap</code>. * * @see <a href="http://www.w3.org/TR/xmlschema-0/#schemaLocation">XML Schema Part 0: Primer * Second Edition | 5.6 schemaLocation</a> * * @param schemaLocationMap {@link Map} to add schema locations to. * @param schemaLocation attribute value to build schema location map from. * @return {@link Map} of schema namespace URIs to location URLs. * @since GemFire 8.1 */ static final Map<String, List<String>> buildSchemaLocationMap( Map<String, List<String>> schemaLocationMap, final String schemaLocation) { if (null == schemaLocation) { return schemaLocationMap; } if (null == schemaLocation || schemaLocation.isEmpty()) { // should really ever be null but being safe. return schemaLocationMap; } final StringTokenizer st = new StringTokenizer(schemaLocation, " \n\t\r"); while (st.hasMoreElements()) { final String ns = st.nextToken(); final String loc = st.nextToken(); List<String> locs = schemaLocationMap.get(ns); if (null == locs) { locs = new ArrayList<>(); schemaLocationMap.put(ns, locs); } if (!locs.contains(loc)) { locs.add(loc); } } return schemaLocationMap; } /***** * Deletes all the node from the document which match the definition provided by xmlentity * * @param doc * @param xmlEntity * @throws Exception */ public static void deleteNode(Document doc, XmlEntity xmlEntity) throws Exception { NodeList nodes = getNodes(doc, xmlEntity); if (nodes != null) { int length = nodes.getLength(); for (int i = 0; i < length; i++) { Node node = nodes.item(i); node.getParentNode().removeChild(node); } } } /**** * Gets all the nodes matching the definition given by the xml entity * * @param doc * @param xmlEntity * @return Nodes * @throws XPathExpressionException */ public static NodeList getNodes(Document doc, XmlEntity xmlEntity) throws XPathExpressionException { return query(doc, xmlEntity.getSearchString(), new XPathContext(xmlEntity.getPrefix(), xmlEntity.getNamespace())); } /** * An object used by an XPath query that maps namespaces to uris. * * @see NamespaceContext * */ public static class XPathContext implements NamespaceContext { private HashMap<String, String> prefixToUri = new HashMap<String, String>(); private HashMap<String, String> uriToPrefix = new HashMap<String, String>(); public XPathContext() {} public XPathContext(String prefix, String uri) { addNamespace(prefix, uri); } public void addNamespace(String prefix, String uri) { this.prefixToUri.put(prefix, uri); this.uriToPrefix.put(uri, prefix); } @Override public String getNamespaceURI(String prefix) { return prefixToUri.get(prefix); } @Override public String getPrefix(String namespaceURI) { return uriToPrefix.get(namespaceURI); } @Override public Iterator<String> getPrefixes(String namespaceURI) { return Collections.singleton(getPrefix(namespaceURI)).iterator(); } } /**** * Converts the document to a well formatted Xml string * * @param doc * @return pretty xml string * @throws IOException * @throws TransformerException * @throws TransformerFactoryConfigurationError */ public static String prettyXml(Node doc) throws IOException, TransformerFactoryConfigurationError, TransformerException { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); return transform(transformer, doc); } public static final String elementToString(Node element) throws TransformerFactoryConfigurationError, TransformerException { Transformer transformer = TransformerFactory.newInstance().newTransformer(); return transform(transformer, element); } private static final String transform(Transformer transformer, Node element) throws TransformerException { StreamResult result = new StreamResult(new StringWriter()); DOMSource source = new DOMSource(element); transformer.transform(source, result); String xmlString = result.getWriter().toString(); return xmlString; } /**** * Convert the xmlString to pretty well formatted xmlString * * @param xmlContent * @return pretty xml string * @throws IOException * @throws TransformerException * @throws TransformerFactoryConfigurationError * @throws ParserConfigurationException * @throws SAXException */ public static String prettyXml(String xmlContent) throws IOException, TransformerFactoryConfigurationError, TransformerException, SAXException, ParserConfigurationException { Document doc = createDocumentFromXml(xmlContent); return prettyXml(doc); } /*** * Create a document from the xml * * @param xmlContent * @return Document * @throws IOException * @throws ParserConfigurationException * @throws SAXException */ public static Document createDocumentFromXml(String xmlContent) throws SAXException, ParserConfigurationException, IOException { return createDocumentFromReader(new StringReader(xmlContent)); } /** * Create a {@link Document} using {@link XmlUtils#createDocumentFromXml(String)} and if the * version attribute is not equal to the current version then update the XML to the current schema * and return the document. * * @param xmlContent XML content to load and upgrade. * @return {@link Document} from xmlContent. * @since GemFire 8.1 */ public static Document createAndUpgradeDocumentFromXml(String xmlContent) throws SAXException, ParserConfigurationException, IOException, XPathExpressionException { Document doc = XmlUtils.createDocumentFromXml(xmlContent); if (!CacheXml.VERSION_LATEST.equals(XmlUtils.getAttribute(doc.getDocumentElement(), CacheXml.VERSION, CacheXml.GEODE_NAMESPACE))) { doc = upgradeSchema(doc, CacheXml.GEODE_NAMESPACE, CacheXml.LATEST_SCHEMA_LOCATION, CacheXml.VERSION_LATEST); } return doc; } /** * Upgrade the schema of a given Config XMl <code>document</code> to the given * <code>namespace</code>, <code>schemaLocation</code> and <code>version</code>. * * @param document Config XML {@link Document} to upgrade. * @param namespaceUri Namespace URI to upgrade to. * @param schemaLocation Schema location to upgrade to. * @throws XPathExpressionException * @throws ParserConfigurationException * @since GemFire 8.1 */ // UnitTest SharedConfigurationTest.testCreateAndUpgradeDocumentFromXml() public static Document upgradeSchema(Document document, final String namespaceUri, final String schemaLocation, String schemaVersion) throws XPathExpressionException, ParserConfigurationException { if (StringUtils.isBlank(namespaceUri)) { throw new IllegalArgumentException("namespaceUri"); } if (StringUtils.isBlank(schemaLocation)) { throw new IllegalArgumentException("schemaLocation"); } if (StringUtils.isBlank(schemaVersion)) { throw new IllegalArgumentException("schemaVersion"); } if (null != document.getDoctype()) { // doc.setDocType(null); Node root = document.getDocumentElement(); Document copiedDocument = getDocumentBuilder().newDocument(); Node copiedRoot = copiedDocument.importNode(root, true); copiedDocument.appendChild(copiedRoot); document = copiedDocument; } final Element root = document.getDocumentElement(); final Map<String, String> namespacePrefixMap = buildNamespacePrefixMap(root); // Add CacheXml namespace if missing. String cachePrefix = namespacePrefixMap.get(namespaceUri); if (null == cachePrefix) { // Default to null prefix. cachePrefix = NULL_NS_URI; // Move all into new namespace changeNamespace(root, NULL_NS_URI, namespaceUri); namespacePrefixMap.put(namespaceUri, cachePrefix); } // Add schema instance namespace if missing. String xsiPrefix = namespacePrefixMap.get(W3C_XML_SCHEMA_INSTANCE_NS_URI); if (null == xsiPrefix) { xsiPrefix = W3C_XML_SCHEMA_INSTANCE_PREFIX; root.setAttribute("xmlns:" + xsiPrefix, W3C_XML_SCHEMA_INSTANCE_NS_URI); namespacePrefixMap.put(W3C_XML_SCHEMA_INSTANCE_NS_URI, xsiPrefix); } // Create schemaLocation attribute if missing. final String schemaLocationAttribute = getAttribute(root, W3C_XML_SCHEMA_INSTANCE_ATTRIBUTE_SCHEMA_LOCATION, W3C_XML_SCHEMA_INSTANCE_NS_URI); // Update schemaLocation for namespace. final Map<String, List<String>> schemaLocationMap = buildSchemaLocationMap(schemaLocationAttribute); List<String> schemaLocations = schemaLocationMap.get(namespaceUri); if (null == schemaLocations) { schemaLocations = new ArrayList<String>(); schemaLocationMap.put(namespaceUri, schemaLocations); } schemaLocations.clear(); schemaLocations.add(schemaLocation); String schemaLocationValue = getSchemaLocationValue(schemaLocationMap); root.setAttributeNS(W3C_XML_SCHEMA_INSTANCE_NS_URI, xsiPrefix + ":" + W3C_XML_SCHEMA_INSTANCE_ATTRIBUTE_SCHEMA_LOCATION, schemaLocationValue); // Set schema version if (cachePrefix == null || cachePrefix.isEmpty()) { root.setAttribute("version", schemaVersion); } else { root.setAttributeNS(namespaceUri, cachePrefix + ":version", schemaVersion); } return document; } /** * Set the <code>schemaLocationAttribute</code> to the values of the * <code>schemaLocationMap</code>. * * @see <a href="http://www.w3.org/TR/xmlschema-0/#schemaLocation">XML Schema Part 0: Primer * Second Edition | 5.6 schemaLocation</a> * * @param schemaLocationMap {@link Map} to get schema locations from. * @since GemFire 8.1 */ private static final String getSchemaLocationValue( final Map<String, List<String>> schemaLocationMap) { final StringBuilder sb = new StringBuilder(); for (final Map.Entry<String, List<String>> entry : schemaLocationMap.entrySet()) { for (final String schemaLocation : entry.getValue()) { if (sb.length() > 0) { sb.append(' '); } sb.append(entry.getKey()).append(' ').append(schemaLocation); } } return sb.toString(); } /** * Build {@link Map} of namespace URIs to prefixes. * * @param root {@link Element} to get namespaces and prefixes from. * @return {@link Map} of namespace URIs to prefixes. * @since GemFire 8.1 */ private static final Map<String, String> buildNamespacePrefixMap(final Element root) { final HashMap<String, String> namespacePrefixMap = new HashMap<>(); // Look for all of the attributes of cache that start with // xmlns NamedNodeMap attributes = root.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Node item = attributes.item(i); if (item.getNodeName().startsWith("xmlns")) { // Anything after the colon is the prefix // eg xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" // has a prefix of xsi String[] splitName = item.getNodeName().split(":"); String prefix; if (splitName.length > 1) { prefix = splitName[1]; } else { prefix = ""; } String uri = item.getTextContent(); namespacePrefixMap.put(uri, prefix); } } return namespacePrefixMap; } /** * Change the namespace URI of a <code>node</code> and its children to * <code>newNamespaceUri</code> if that node is in the given <code>oldNamespaceUri</code> * namespace URI. * * * @param node {@link Node} to change namespace URI on. * @param oldNamespaceUri old namespace URI to change from. * @param newNamespaceUri new Namespace URI to change to. * @throws XPathExpressionException * @return the modified version of the passed in node. * @since GemFire 8.1 */ static final Node changeNamespace(final Node node, final String oldNamespaceUri, final String newNamespaceUri) throws XPathExpressionException { Node result = null; final NodeList nodes = query(node, "//*"); for (int i = 0; i < nodes.getLength(); i++) { final Node element = nodes.item(i); if (element.getNamespaceURI() == null || element.getNamespaceURI().equals(oldNamespaceUri)) { Node renamed = node.getOwnerDocument().renameNode(element, newNamespaceUri, element.getNodeName()); if (element == node) { result = renamed; } } } return result; } /**** * Method to modify the root attribute (cache) of the XML * * @param doc Target document whose root attributes must be modified * @param xmlEntity xml entity for the root , it also contains the attributes * @throws IOException */ public static void modifyRootAttributes(Document doc, XmlEntity xmlEntity) throws IOException { if (xmlEntity == null || xmlEntity.getAttributes() == null) { return; } String type = xmlEntity.getType(); Map<String, String> attributes = xmlEntity.getAttributes(); Element root = doc.getDocumentElement(); if (root.getLocalName().equals(type)) { for (Entry<String, String> entry : attributes.entrySet()) { String attributeName = entry.getKey(); String attributeValue = entry.getValue(); // Remove the existing attribute String rootAttribute = getAttribute(root, attributeName); if (null != rootAttribute) { root.removeAttribute(rootAttribute); } // Add the new attribute with new value root.setAttribute(attributeName, attributeValue); } } } }