/* * Copyright (c) 2010-2013 Evolveum * * 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 com.evolveum.midpoint.util; import static javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI; import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.util.*; import java.util.Map.Entry; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; 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.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.sun.org.apache.xml.internal.utils.XMLChar; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; import org.w3c.dom.Attr; import org.w3c.dom.Comment; 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.w3c.dom.Text; import org.xml.sax.SAXException; import com.evolveum.midpoint.util.exception.SystemException; /** * @author Igor Farinic * @author Radovan Semancik * @since 0.1 */ public class DOMUtil { public static final Trace LOGGER = TraceManager.getTrace(DOMUtil.class); public static final String W3C_XML_SCHEMA_XMLNS_URI = "http://www.w3.org/2000/xmlns/"; public static final String W3C_XML_SCHEMA_XMLNS_PREFIX = "xmlns"; public static final String W3C_XML_XML_URI = "http://www.w3.org/XML/1998/namespace"; public static final String W3C_XML_XML_PREFIX = "xml"; public static final String NS_W3C_XSI_PREFIX = "xsi"; public static final QName XSI_TYPE = new QName(W3C_XML_SCHEMA_INSTANCE_NS_URI, "type", NS_W3C_XSI_PREFIX); public static final QName XSI_NIL = new QName(W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil", NS_W3C_XSI_PREFIX); public static final QName XML_ID_ATTRIBUTE = new QName(W3C_XML_XML_URI, "id", W3C_XML_XML_PREFIX); public static final String HACKED_XSI_TYPE = "xsiType"; public static final String IS_LIST_ATTRIBUTE_NAME = "list"; private static final List<String> AUXILIARY_ATTRIBUTE_NAMES = Arrays.asList(HACKED_XSI_TYPE, IS_LIST_ATTRIBUTE_NAME); private static final List<String> AUXILIARY_NAMESPACES = Arrays.asList(W3C_XML_SCHEMA_XMLNS_URI, W3C_XML_XML_URI, W3C_XML_SCHEMA_INSTANCE_NS_URI); public static final String NS_W3C_XML_SCHEMA_PREFIX = "xsd"; public static final QName XSD_SCHEMA_ELEMENT = new QName(W3C_XML_SCHEMA_NS_URI, "schema", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ANNOTATION_ELEMENT = new QName(W3C_XML_SCHEMA_NS_URI, "annotation", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_APPINFO_ELEMENT = new QName(W3C_XML_SCHEMA_NS_URI, "appinfo", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_DOCUMENTATION_ELEMENT = new QName(W3C_XML_SCHEMA_NS_URI, "documentation", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_IMPORT_ELEMENT = new QName(W3C_XML_SCHEMA_NS_URI, "import", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_INCLUDE_ELEMENT = new QName(W3C_XML_SCHEMA_NS_URI, "include", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ATTR_TARGET_NAMESPACE = new QName(W3C_XML_SCHEMA_NS_URI, "targetNamespace", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ATTR_NAMESPACE = new QName(W3C_XML_SCHEMA_NS_URI, "namespace", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ATTR_SCHEMA_LOCATION = new QName(W3C_XML_SCHEMA_NS_URI, "schemaLocation", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_DECIMAL = new QName(W3C_XML_SCHEMA_NS_URI, "decimal", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_STRING = new QName(W3C_XML_SCHEMA_NS_URI, "string", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_INTEGER = new QName(W3C_XML_SCHEMA_NS_URI, "integer", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_INT = new QName(W3C_XML_SCHEMA_NS_URI, "int", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_LONG = new QName(W3C_XML_SCHEMA_NS_URI, "long", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_SHORT = new QName(W3C_XML_SCHEMA_NS_URI, "short", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_FLOAT = new QName(W3C_XML_SCHEMA_NS_URI, "float", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_DOUBLE = new QName(W3C_XML_SCHEMA_NS_URI, "double", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_BOOLEAN = new QName(W3C_XML_SCHEMA_NS_URI, "boolean", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_BASE64BINARY = new QName(W3C_XML_SCHEMA_NS_URI, "base64Binary", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_DATETIME = new QName(W3C_XML_SCHEMA_NS_URI, "dateTime", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_DURATION = new QName(W3C_XML_SCHEMA_NS_URI, "duration", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_BYTE = new QName(W3C_XML_SCHEMA_NS_URI, "byte", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_QNAME = new QName(W3C_XML_SCHEMA_NS_URI, "QName", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ANYURI = new QName(W3C_XML_SCHEMA_NS_URI, "anyURI", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ANY = new QName(W3C_XML_SCHEMA_NS_URI, "any", NS_W3C_XML_SCHEMA_PREFIX); public static final QName XSD_ANYTYPE = new QName(W3C_XML_SCHEMA_NS_URI, "anyType", NS_W3C_XML_SCHEMA_PREFIX); public static final String NS_XML_ENC = "http://www.w3.org/2001/04/xmlenc#"; public static final String NS_XML_DSIG = "http://www.w3.org/2000/09/xmldsig#"; public static final String NS_WSDL = "http://schemas.xmlsoap.org/wsdl/"; public static final String NS_WSDL_SCHEMA_PREFIX = "wsdl"; public static final QName WSDL_IMPORT_ELEMENT = new QName(NS_WSDL, "import", NS_WSDL_SCHEMA_PREFIX); public static final QName WSDL_TYPES_ELEMENT = new QName(NS_WSDL, "types", NS_WSDL_SCHEMA_PREFIX); public static final QName WSDL_ATTR_NAMESPACE = new QName(NS_WSDL, "namespace", NS_WSDL_SCHEMA_PREFIX); public static final QName WSDL_ATTR_SCHEMA_LOCATION = new QName(NS_WSDL, "schemaLocation", NS_WSDL_SCHEMA_PREFIX); public static final QName WSDL_ATTR_LOCATION = new QName(NS_WSDL, "location", NS_WSDL_SCHEMA_PREFIX); private static final String RANDOM_ATTR_PREFIX_PREFIX = "qn"; private static final int RANDOM_ATTR_PREFIX_RND = 1000; private static final int RANDOM_ATTR_PREFIX_MAX_ITERATIONS = 30; // To generate random namespace prefixes private static Random rnd = new Random(); private static final DocumentBuilder loader; static { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); loader = factory.newDocumentBuilder(); } catch (ParserConfigurationException ex) { throw new IllegalStateException("Error creating XML document " + ex.getMessage()); } } public static String serializeDOMToString(org.w3c.dom.Node node) { return printDom(node).toString(); } public static void serializeDOMToFile(org.w3c.dom.Node node, File file) throws TransformerFactoryConfigurationError, TransformerException { Transformer transformer = TransformerFactory.newInstance().newTransformer(); Result output = new StreamResult(file); Source input = new DOMSource(node); transformer.transform(input, output); } public static Document getDocument(Node node) { if (node instanceof Document) { return (Document) node; } return node.getOwnerDocument(); } public static Document getDocument() { return loader.newDocument(); } public static Document getDocument(QName rootElementName) { Document document = loader.newDocument(); document.appendChild(createElement(document, rootElementName)); return document; } public static DocumentBuilder createDocumentBuilder() { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); try { return factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new IllegalStateException("Error creating document builder " + e.getMessage(), e); } } public static Document parseDocument(String doc) { try { DocumentBuilder loader = createDocumentBuilder(); return loader.parse(IOUtils.toInputStream(doc, "utf-8")); } catch (SAXException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } catch (IOException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } } public static Document parseFile(String filePath) { return parseFile(new File(filePath)); } public static Document parseFile(File file) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder loader = factory.newDocumentBuilder(); return loader.parse(file); } catch (SAXException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } catch (IOException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } catch (ParserConfigurationException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } } public static Document parse(InputStream inputStream) throws IOException { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); factory.setFeature("http://xml.org/sax/features/namespaces", true); // voodoo to turn off reading of DTDs during parsing. This is needed e.g. to pre-parse schemas factory.setValidating(false); factory.setFeature("http://xml.org/sax/features/validation", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); DocumentBuilder loader = factory.newDocumentBuilder(); return loader.parse(inputStream); } catch (SAXException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } catch (ParserConfigurationException ex) { throw new IllegalStateException("Error parsing XML document " + ex.getMessage(),ex); } } public static String showDom(List<Element> elements) { StringBuilder sb = new StringBuilder(); for (Element element : elements) { showDomNode(element, sb, 0); sb.append("\n"); } return sb.toString(); } public static StringBuffer printDom(Node node) { return printDom(node, true, true); } public static StringBuffer printDom(Node node, boolean indent, boolean omitXmlDeclaration) { StringWriter writer = new StringWriter(); TransformerFactory transfac = TransformerFactory.newInstance(); Transformer trans; try { trans = transfac.newTransformer(); } catch (TransformerConfigurationException e) { throw new SystemException("Error in XML configuration: "+e.getMessage(),e); } trans.setOutputProperty(OutputKeys.INDENT, (indent ? "yes" : "no")); trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); // XALAN-specific trans.setParameter(OutputKeys.ENCODING, "utf-8"); // Note: serialized XML does not contain xml declaration trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, (omitXmlDeclaration ? "yes" : "no")); DOMSource source = new DOMSource(node); try { trans.transform(source, new StreamResult(writer)); } catch (TransformerException e) { throw new SystemException("Error in XML transformation: "+e.getMessage(),e); } return writer.getBuffer(); } private static void showDomNode(Node node, StringBuilder sb, int level) { if (sb == null) { // buffer not provided, return immediately return; } // indent for (int i = 0; i < level; i++) { sb.append(" "); } if (node == null) { sb.append("null\n"); } else { sb.append(node.getNodeName()); sb.append(" ("); NamedNodeMap attributes = node.getAttributes(); boolean broken = false; if (attributes != null) { for (int ii = 0; ii < attributes.getLength(); ii++) { Node attribute = attributes.item(ii); sb.append(attribute.getPrefix()); sb.append(":"); sb.append(attribute.getLocalName()); sb.append("='"); sb.append(attribute.getNodeValue()); sb.append("',"); if (attribute.getPrefix() == null && attribute.getLocalName().equals("xmlns") && (attribute.getNodeValue() == null || attribute.getNodeValue().isEmpty())) { broken = true; } } } sb.append(")"); if (broken) { sb.append(" *** WARNING: empty default namespace"); } sb.append("\n"); NodeList childNodes = node.getChildNodes(); for (int ii = 0; ii < childNodes.getLength(); ii++) { Node subnode = childNodes.item(ii); showDomNode(subnode, sb, level + 1); } } } public static Node getNextSiblingElement(Node node) { if (node == null || node.getParentNode() == null) { return null; } Node parent = node.getParentNode(); NodeList nodes = parent.getChildNodes(); if (nodes == null) { return null; } boolean found = false; for (int i = 0; i < nodes.getLength(); i++) { Node child = nodes.item(i); if (child.equals(node)) { found = true; continue; } if (found && child.getNodeType() == Node.ELEMENT_NODE) { return child; } } return null; } public static Element getFirstChildElement(Node parent) { if (parent == null || parent.getChildNodes() == null) { return null; } NodeList nodes = parent.getChildNodes(); for (int i = 0; i < nodes.getLength(); i++) { Node child = nodes.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { return (Element) child; } } return null; } public static Element getLastChildElement(Node parent) { if (parent == null || parent.getChildNodes() == null) { return null; } NodeList nodes = parent.getChildNodes(); for (int i = nodes.getLength() - 1; i >= 0; i--) { Node child = nodes.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { return (Element) child; } } return null; } public static List<Element> getChildElements(Element element, QName elementName){ Validate.notNull(elementName, "Element name to get must not be null"); List<Element> elements = new ArrayList<Element>(); NodeList childNodes = element.getChildNodes(); for (int i= 0; i< childNodes.getLength(); i++){ Node childNode = childNodes.item(i); if (QNameUtil.compareQName(elementName, childNode)){ elements.add((Element)childNode); } } return elements; } @NotNull public static List<Element> listChildElements(Node node) { List<Element> subelements = new ArrayList<>(); NodeList childNodes = node.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node childNode = childNodes.item(i); if (childNode.getNodeType() == Node.ELEMENT_NODE) { subelements.add((Element) childNode); } } return subelements; } public static boolean hasChildElements(Node node) { List<Element> childElements = listChildElements(node); return (!childElements.isEmpty()); } public static QName resolveQName(Element element) { return resolveQName(element, element.getTextContent()); } /** * Resolves a QName. * * @param domNode Provides a context in which we will resolve namespace prefixes (may be null) * @param qnameStringRepresentation String representation of a QName (e.g. c:RoleType) (may be null) * * @return parsed QName (or null if string representation is blank) * * Contrary to traditional XML handling, a QName without prefix is parsed to a QName without namespace, * even if default namespace declaration is present. */ public static QName resolveQName(Node domNode, String qnameStringRepresentation) { if (StringUtils.isBlank(qnameStringRepresentation)) { // No QName return null; } String[] qnameArray = qnameStringRepresentation.split(":"); if (qnameArray.length > 2) { throw new IllegalArgumentException("Unsupported format: more than one colon in Qname: " + qnameStringRepresentation); } QName qname; if (qnameArray.length == 1 || qnameArray[1] == null || qnameArray[1].isEmpty()) { // no prefix => no namespace qname = new QName(null, qnameArray[0]); } else { String namespacePrefix = qnameArray[0]; String namespace = findNamespace(domNode, namespacePrefix); if (namespace == null) { QNameUtil.reportUndeclaredNamespacePrefix(namespacePrefix, qnameStringRepresentation); namespacePrefix = QNameUtil.markPrefixAsUndeclared(namespacePrefix); } qname = new QName(namespace, qnameArray[1], namespacePrefix); } return qname; } public static String findNamespace(Node domNode, String prefix) { String ns = null; if (domNode != null) { if (prefix == null || prefix.isEmpty()) { ns = domNode.lookupNamespaceURI(null); } else { ns = domNode.lookupNamespaceURI(prefix); } if (ns != null) { return ns; } } return ns; } public static QName resolveXsiType(Element element) { String xsiType = element.getAttributeNS(XSI_TYPE.getNamespaceURI(), XSI_TYPE.getLocalPart()); if (xsiType == null || xsiType.isEmpty()) { xsiType = element.getAttribute(HACKED_XSI_TYPE); } if (xsiType == null || xsiType.isEmpty()) { return null; } return resolveQName(element, xsiType); } public static boolean hasXsiType(Element element) { String xsiType = element.getAttributeNS(XSI_TYPE.getNamespaceURI(), XSI_TYPE.getLocalPart()); if (xsiType == null || xsiType.isEmpty()) { xsiType = element.getAttribute(HACKED_XSI_TYPE); } if (xsiType == null || xsiType.isEmpty()) { return false; } return true; } public static void removeXsiType(Element element) { element.removeAttributeNS(XSI_TYPE.getNamespaceURI(), XSI_TYPE.getLocalPart()); element.removeAttribute(HACKED_XSI_TYPE); } public static void setXsiType(Element element, QName type) { if (hasXsiType(element)) { throw new IllegalArgumentException("Element already has a type"); } setQNameAttribute(element, XSI_TYPE, type); } public static void setQNameAttribute(Element element, QName attributeName, QName attributeValue) { Document doc = element.getOwnerDocument(); Attr attr = doc.createAttributeNS(attributeName.getNamespaceURI(), attributeName.getLocalPart()); String namePrefix = lookupOrCreateNamespaceDeclaration(element, attributeName.getNamespaceURI(), attributeName.getPrefix(), element, true); attr.setPrefix(namePrefix); setQNameAttribute(element, attr, attributeValue, element); } public static void setQNameAttribute(Element element, String attributeName, QName attributeValue) { Document doc = element.getOwnerDocument(); Attr attr = doc.createAttribute(attributeName); setQNameAttribute(element, attr, attributeValue, element); } public static void setQNameAttribute(Element element, QName attributeName, QName attributeValue, Element definitionElement) { Document doc = element.getOwnerDocument(); Attr attr = doc.createAttributeNS(attributeName.getNamespaceURI(), attributeName.getLocalPart()); String namePrefix = lookupOrCreateNamespaceDeclaration(element, attributeName.getNamespaceURI(), attributeName.getPrefix(), element, true); attr.setPrefix(namePrefix); setQNameAttribute(element, attr, attributeValue, definitionElement); } public static void setQNameAttribute(Element element, String attributeName, QName attributeValue, Element definitionElement) { Document doc = element.getOwnerDocument(); Attr attr = doc.createAttribute(attributeName); setQNameAttribute(element, attr, attributeValue, definitionElement); } /* * Actually, it is not possible to create *and use* xmlns declaration pointing to an empty URI. From Section 6.1 in http://www.w3.org/TR/xml-names11/ * * <?xml version="1.1"?> * <x xmlns:n1="http://www.w3.org"> * <n1:a/> <!-- legal; the prefix n1 is bound to http://www.w3.org --> * <x xmlns:n1=""> * <n1:a/> <!-- illegal; the prefix n1 is not bound here --> * <x xmlns:n1="http://www.w3.org"> * <n1:a/> <!-- legal; the prefix n1 is bound again --> * </x> * </x> * </x> * * We strictly use localname-only representation of QNames with null NS. When writing, we write it in such a way. * And when reading, we ignore default namespace when parsing unqualified QNames. */ private static void setQNameAttribute(Element element, Attr attr, QName attributeQnameValue, Element definitionElement) { String attributeStringValue; if (attributeQnameValue == null) { attributeStringValue = ""; } else if (XMLConstants.NULL_NS_URI.equals(attributeQnameValue.getNamespaceURI())) { if (QNameUtil.isPrefixUndeclared(attributeQnameValue.getPrefix())) { attributeStringValue = attributeQnameValue.getPrefix() + ":" + attributeQnameValue.getLocalPart(); // to give user a chance to see and fix this } else { attributeStringValue = attributeQnameValue.getLocalPart(); } } else { String valuePrefix = lookupOrCreateNamespaceDeclaration(element, attributeQnameValue.getNamespaceURI(), attributeQnameValue.getPrefix(), definitionElement, false); assert StringUtils.isNotBlank(valuePrefix); attributeStringValue = valuePrefix + ":" + attributeQnameValue.getLocalPart(); } NamedNodeMap attributes = element.getAttributes(); checkValidXmlChars(attributeStringValue); attr.setValue(attributeStringValue); attributes.setNamedItem(attr); } /** * Sets QName value for a given element. * * Contrary to standard XML semantics, namespace-less QNames are specified as simple names without prefix * (regardless of default prefix used in the XML document). * * @param element Element whose text content should be set to represent QName value * @param elementValue QName value to be stored into the element */ public static void setQNameValue(Element element, QName elementValue) { if (elementValue == null) { setElementTextContent(element, ""); } else if (XMLConstants.NULL_NS_URI.equals(elementValue.getNamespaceURI())) { if (QNameUtil.isPrefixUndeclared(elementValue.getPrefix())) { setElementTextContent(element, elementValue.getPrefix() + ":" + elementValue.getLocalPart()); } else { setElementTextContent(element, elementValue.getLocalPart()); } } else { String prefix = lookupOrCreateNamespaceDeclaration(element, elementValue.getNamespaceURI(), elementValue.getPrefix(), element, false); assert StringUtils.isNotBlank(prefix); String stringValue = prefix + ":" + elementValue.getLocalPart(); setElementTextContent(element, stringValue); } } /** * @param element Element, on which the namespace declaration is evaluated * @param namespaceUri Namespace URI to be assigned to a prefix * @param preferredPrefix Preferred prefix * @param definitionElement Element, on which namespace declaration will be created (there should not be any redefinitions between definitionElement and element in order for this to work...) * @param allowUseOfDefaultNamespace If we are allowed to use default namespace (i.e. return empty prefix). This is important for QNames, see setQNameValue * @return prefix that is really used * * Returned prefix is never null nor "" if allowUseOfDefaultNamespace is false. */ public static String lookupOrCreateNamespaceDeclaration(Element element, String namespaceUri, String preferredPrefix, Element definitionElement, boolean allowUseOfDefaultNamespace) { // We need to figure out correct prefix. We have namespace URI, but we // need a prefix to specify in the xsi:type or element name if (!StringUtils.isBlank(preferredPrefix)) { String namespaceForPreferredPrefix = element.lookupNamespaceURI(preferredPrefix); if (namespaceForPreferredPrefix == null) { // preferred prefix is not yet bound setNamespaceDeclaration(definitionElement, preferredPrefix, namespaceUri); return preferredPrefix; } else { if (namespaceForPreferredPrefix.equals(namespaceUri)) { return preferredPrefix; } else { // Prefix conflict, we need to create different prefix // Just going on will do that } } } if (allowUseOfDefaultNamespace && element.isDefaultNamespace(namespaceUri)) { // Namespace URI is a default namespace. Return empty prefix; return ""; } // We DO NOT WANT to use default namespace for QNames. QNames without prefix are NOT considered by midPoint to belong to the default namespace. String prefix = element.lookupPrefix(namespaceUri); if (prefix == null) { // generate random prefix boolean gotIt = false; for (int i=0; i < RANDOM_ATTR_PREFIX_MAX_ITERATIONS; i++) { prefix = generatePrefix(); if (element.lookupNamespaceURI(prefix) == null) { // the prefix is free gotIt = true; break; } } if (!gotIt) { throw new IllegalStateException("Unable to generate unique prefix for namespace "+namespaceUri+" even after "+RANDOM_ATTR_PREFIX_MAX_ITERATIONS+" attempts"); } setNamespaceDeclaration(definitionElement, prefix, namespaceUri); } return prefix; } private static String generatePrefix() { return RANDOM_ATTR_PREFIX_PREFIX + rnd.nextInt(RANDOM_ATTR_PREFIX_RND); } public static boolean isNamespaceDefinition(Attr attr) { if(W3C_XML_SCHEMA_XMLNS_URI.equals(attr.getNamespaceURI())) { return true; } if(attr.getName().startsWith("xmlns:") || "xmlns".equals(attr.getName())) { return true; } return false; } public static void setNamespaceDeclaration(Element element, String prefix, String namespaceUri) { Document doc = element.getOwnerDocument(); NamedNodeMap attributes = element.getAttributes(); Attr attr; if (prefix == null || prefix.isEmpty()) { // default namespace attr = doc.createAttributeNS(W3C_XML_SCHEMA_XMLNS_URI, W3C_XML_SCHEMA_XMLNS_PREFIX); } else { attr = doc .createAttributeNS(W3C_XML_SCHEMA_XMLNS_URI, W3C_XML_SCHEMA_XMLNS_PREFIX + ":" + prefix); } checkValidXmlChars(namespaceUri); attr.setValue(namespaceUri); attributes.setNamedItem(attr); } /** * Returns map of all namespace declarations from specified element (prefix -> namespace). */ public static Map<String,String> getNamespaceDeclarations(Element element) { Map<String,String> nsDeclMap = new HashMap<String, String>(); NamedNodeMap attributes = element.getAttributes(); for(int i=0; i<attributes.getLength(); i++) { Attr attr = (Attr)attributes.item(i); if (isNamespaceDefinition(attr)) { String prefix = getNamespaceDeclarationPrefix(attr); String namespace = getNamespaceDeclarationNamespace(attr); nsDeclMap.put(prefix, namespace); } } return nsDeclMap; } public static void setNamespaceDeclarations(Element element, Map<String, String> rootNamespaceDeclarations) { if (rootNamespaceDeclarations == null) { return; } for (Entry<String, String> entry : rootNamespaceDeclarations.entrySet()) { setNamespaceDeclaration(element, entry.getKey(), entry.getValue()); } } /** * Returns all namespace declarations visible from the given node. * Uses recursion for simplicity. */ public static Map<String,String> getAllVisibleNamespaceDeclarations(Node node) { Map<String,String> retval; Node parent = getParentNode(node); if (parent != null) { retval = getAllVisibleNamespaceDeclarations(parent); } else { retval = new HashMap<>(); } if (node instanceof Element) { retval.putAll(getNamespaceDeclarations((Element) node)); } return retval; } // returns owner node - works also for attributes private static Node getParentNode(Node node) { if (node instanceof Attr) { Attr attr = (Attr) node; return attr.getOwnerElement(); } else { return node.getParentNode(); } } /** * Take all the namespace declaration of parent elements and put them to this element. */ public static void fixNamespaceDeclarations(Element element) { fixNamespaceDeclarations(element, element); } private static void fixNamespaceDeclarations(Element targetElement, Element currentElement) { NamedNodeMap attributes = currentElement.getAttributes(); for(int i=0; i<attributes.getLength(); i++) { Attr attr = (Attr)attributes.item(i); if (isNamespaceDefinition(attr)) { String prefix = getNamespaceDeclarationPrefix(attr); String namespace = getNamespaceDeclarationNamespace(attr); if (hasNamespaceDeclarationForPrefix(targetElement, prefix)) { if (targetElement != currentElement) { // We are processing parent element, while the original element already // has prefix declaration. That means it must have been processed before // we can skip the usage check continue; } } else { setNamespaceDeclaration(targetElement, prefix, getNamespaceDeclarationNamespace(attr)); } } } Node parentNode = currentElement.getParentNode(); if (parentNode instanceof Element) { fixNamespaceDeclarations(targetElement, (Element)parentNode); } } public static boolean isPrefixUsed(Element targetElement, String prefix) { if (comparePrefix(prefix, targetElement.getPrefix())) { return true; } NamedNodeMap attributes = targetElement.getAttributes(); for(int i=0; i<attributes.getLength(); i++) { Attr attr = (Attr)attributes.item(i); if (comparePrefix(prefix, attr.getPrefix())) { return true; } } NodeList childNodes = targetElement.getChildNodes(); for (int i=0; i<childNodes.getLength(); i++) { Node node = childNodes.item(i); if (node instanceof Element) { Element element = (Element)node; if (isPrefixUsed(element, prefix)) { return true; } } } return false; } public static boolean hasNamespaceDeclarationForPrefix(Element targetElement, String prefix) { return getNamespaceDeclarationForPrefix(targetElement, prefix) != null; } public static String getNamespaceDeclarationForPrefix(Element targetElement, String prefix) { NamedNodeMap attributes = targetElement.getAttributes(); for(int i=0; i<attributes.getLength(); i++) { Attr attr = (Attr)attributes.item(i); if (isNamespaceDefinition(attr)) { String thisPrefix = getNamespaceDeclarationPrefix(attr); if (comparePrefix(prefix, thisPrefix)) { return getNamespaceDeclarationNamespace(attr); } } } return null; } public static String getNamespaceDeclarationPrefix(Attr attr) { if(!W3C_XML_SCHEMA_XMLNS_URI.equals(attr.getNamespaceURI())) { throw new IllegalStateException("Attempt to get prefix from a attribute that is not a namespace declaration, it has namespace " + attr.getNamespaceURI()); } String attrName = attr.getName(); if(attrName.startsWith("xmlns:")) { return attrName.substring(6); } if ("xmlns".equals(attrName)) { return null; } throw new IllegalStateException("Attempt to get prefix from a attribute that is not a namespace declaration, it is "+attrName); } public static String getNamespaceDeclarationNamespace(Attr attr) { if(!W3C_XML_SCHEMA_XMLNS_URI.equals(attr.getNamespaceURI())) { throw new IllegalStateException("Attempt to get namespace from a attribute that is not a namespace declaration, it has namespace " + attr.getNamespaceURI()); } String attrName = attr.getName(); if(!attrName.startsWith("xmlns:") && !"xmlns".equals(attr.getName())) { throw new IllegalStateException("Attempt to get namespace from a attribute that is not a namespace declaration, it is "+attrName); } return attr.getValue(); } public static Collection<Attr> listApplicationAttributes(Element element) { Collection<Attr> attrs = new ArrayList<>(); NamedNodeMap attributes = element.getAttributes(); for(int i=0; i<attributes.getLength(); i++) { Attr attr = (Attr)attributes.item(i); if (isApplicationAttribute(attr)) { attrs.add(attr); } } return attrs; } public static boolean hasApplicationAttributes(Element element) { NamedNodeMap attributes = element.getAttributes(); for(int i=0; i<attributes.getLength(); i++) { Attr attr = (Attr)attributes.item(i); if (isApplicationAttribute(attr)) { return true; } } return false; } private static boolean isApplicationAttribute(Attr attr) { String namespaceURI = attr.getNamespaceURI(); if (StringUtils.isEmpty(namespaceURI)) { return !AUXILIARY_ATTRIBUTE_NAMES.contains(attr.getName()); } else { return !AUXILIARY_NAMESPACES.contains(namespaceURI); } } private static boolean comparePrefix(String prefixA, String prefixB) { if (StringUtils.isBlank(prefixA) && StringUtils.isBlank(prefixB)) { return true; } if (StringUtils.isBlank(prefixA) || StringUtils.isBlank(prefixB)) { return false; } return prefixA.equals(prefixB); } public static Element getChildElement(Element element, QName qname) { for (Element subelement: listChildElements(element)) { if (qname.equals(getQName(subelement))) { return subelement; } } return null; } public static Element getChildElement(Element element, String localPart) { for (Element subelement: listChildElements(element)) { if (subelement.getLocalName().equals(localPart)) { return subelement; } } return null; } public static Element getChildElement(Element element, int index) { return listChildElements(element).get(index); } public static Element getOrCreateAsFirstElement(Element parentElement, QName elementQName) { Element element = getChildElement(parentElement, elementQName); if (element != null) { return element; } Document doc = parentElement.getOwnerDocument(); element = doc.createElementNS(elementQName.getNamespaceURI(), elementQName.getLocalPart()); parentElement.insertBefore(element, getFirstChildElement(parentElement)); return element; } @NotNull public static QName getQName(Element element) { QName name = getQName((Node) element); if (name == null) { throw new IllegalStateException("Element with no name: " + element); } else { return name; } } public static QName getQName(Node node) { if (node.getLocalName() == null) { if (node.getNodeName() == null) { return null; } else { return new QName(null, node.getNodeName()); } } if (node.getPrefix() == null) { return new QName(node.getNamespaceURI(), node.getLocalName()); } return new QName(node.getNamespaceURI(), node.getLocalName(), node.getPrefix()); } public static QName getQNameValue(Element element) { return resolveQName(element, element.getTextContent()); } public static QName getQNameAttribute(Element element, String attributeName) { String attrContent = element.getAttribute(attributeName); if (StringUtils.isBlank(attrContent)) { return null; } return resolveQName(element, attrContent); } public static QName getQNameAttribute(Element element, QName attributeName) { String attrContent = element.getAttributeNS(attributeName.getNamespaceURI(), attributeName.getLocalPart()); if (StringUtils.isBlank(attrContent)) { return null; } return resolveQName(element, attrContent); } public static QName getQNameValue(Attr attr) { return resolveQName(attr, attr.getTextContent()); } public static Integer getIntegerValue(Element element) { if (element == null) { return null; } String textContent = element.getTextContent(); if (StringUtils.isBlank(textContent)) { return null; } return Integer.valueOf(textContent); } public static void copyContent(Element source, Element destination) { NamedNodeMap attributes = source.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Attr attr = (Attr) attributes.item(i); Attr clone = (Attr) attr.cloneNode(true); destination.setAttributeNode(clone); } NodeList childNodes = source.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node item = childNodes.item(i); destination.appendChild(item); } } public static Element createElement(QName qname) { return createElement(DOMUtil.getDocument(), qname); } public static Element createElement(Document document, QName qname) { Element element; // String namespaceURI = qname.getNamespaceURI(); // if (StringUtils.isBlank(namespaceURI)) { // element = document.createElement(qname.getLocalPart()); // } else { element = document.createElementNS(qname.getNamespaceURI(), qname.getLocalPart()); // } if (StringUtils.isNotEmpty(qname.getPrefix()) && StringUtils.isNotEmpty(qname.getNamespaceURI())) { // second part of the condition is because of wrong data in tests (undeclared prefixes in XPath expressions) element.setPrefix(qname.getPrefix()); } return element; } public static Element createElement(Document document, QName qname, Element parentElement, Element definitionElement) { lookupOrCreateNamespaceDeclaration(parentElement, qname.getNamespaceURI(), qname.getPrefix(), definitionElement, true); return createElement(document, qname); } public static Element createSubElement(Element parent, QName subElementQName) { Document doc = parent.getOwnerDocument(); Element subElement = createElement(doc, subElementQName); parent.appendChild(subElement); return subElement; } public static boolean compareElement(Element a, Element b, boolean considerNamespacePrefixes) { return compareElement(a, b, considerNamespacePrefixes, true); } public static boolean compareElement(Element a, Element b, boolean considerNamespacePrefixes, boolean considerWhitespaces) { if (a==b) { return true; } if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } if (!getQName(a).equals(getQName(b))) { return false; } if (!compareAttributes(a.getAttributes(),b.getAttributes(), considerNamespacePrefixes)) { return false; } if (!compareNodeList(a.getChildNodes(),b.getChildNodes(), considerNamespacePrefixes, considerWhitespaces)) { return false; } return true; } public static boolean compareDocument(Document a, Document b, boolean considerNamespacePrefixes, boolean considerWhitespaces) { if (a==b) { return true; } if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } if (!compareNodeList(a.getChildNodes(),b.getChildNodes(), considerNamespacePrefixes, considerWhitespaces)) { return false; } return true; } public static boolean compareElementList(List<Element> aList, List<Element> bList, boolean considerNamespacePrefixes) { return compareElementList(aList, bList, considerNamespacePrefixes, true); } public static boolean compareElementList(List<Element> aList, List<Element> bList, boolean considerNamespacePrefixes, boolean considerWhitespaces) { if (aList.size() != bList.size()) { return false; } Iterator<Element> bIterator = bList.iterator(); for (Element a: aList) { Element b = bIterator.next(); if (!compareElement(a, b, considerNamespacePrefixes, considerWhitespaces)) { return false; } } return true; } private static boolean compareAttributes(NamedNodeMap a, NamedNodeMap b, boolean considerNamespacePrefixes) { if (a==b) { return true; } if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } return (compareAttributesIsSubset(a,b,considerNamespacePrefixes) && compareAttributesIsSubset(b,a,considerNamespacePrefixes)); } private static boolean compareAttributesIsSubset(NamedNodeMap subset, NamedNodeMap superset, boolean considerNamespacePrefixes) { for (int i = 0; i < subset.getLength(); i++) { Node aItem = subset.item(i); Attr aAttr = (Attr) aItem; if (!considerNamespacePrefixes && isNamespaceDefinition(aAttr)) { continue; } if (StringUtils.isBlank(aAttr.getLocalName())) { // this is strange, but it can obviously happen continue; } QName aQname = new QName(aAttr.getNamespaceURI(),aAttr.getLocalName()); Attr bAttr = findAttributeByQName(superset,aQname); if (bAttr == null) { return false; } if (!StringUtils.equals(aAttr.getTextContent(),bAttr.getTextContent())) { return false; } } return true; } private static Attr findAttributeByQName(NamedNodeMap attrs, QName qname) { for (int i = 0; i < attrs.getLength(); i++) { Node aItem = attrs.item(i); Attr aAttr = (Attr) aItem; if (aAttr.getLocalName() == null) { continue; } QName aQname = new QName(aAttr.getNamespaceURI(), aAttr.getLocalName()); if (aQname.equals(qname)) { return aAttr; } } return null; } private static boolean compareNodeList(NodeList a, NodeList b, boolean considerNamespacePrefixes, boolean considerWhitespaces) { if (a==b) { return true; } if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } List<Node> aList = canonizeNodeList(a); List<Node> bList = canonizeNodeList(b); if (aList.size() != bList.size()) { return false; } Iterator<Node> aIterator = aList.iterator(); Iterator<Node> bIterator = bList.iterator(); while (aIterator.hasNext()) { Node aItem = aIterator.next(); Node bItem = bIterator.next(); if (aItem.getNodeType() != bItem.getNodeType()) { return false; } if (aItem.getNodeType() == Node.ELEMENT_NODE) { if (!compareElement((Element)aItem, (Element)bItem, considerNamespacePrefixes, considerWhitespaces)) { return false; } } else if (aItem.getNodeType() == Node.TEXT_NODE) { if (!compareTextNodeValues(aItem.getTextContent(), bItem.getTextContent(), considerWhitespaces)) { return false; } } } return true; } public static boolean compareTextNodeValues(String a, String b) { return compareTextNodeValues(a, b, true); } public static boolean compareTextNodeValues(String a, String b, boolean considerWhitespaces) { if (StringUtils.equals(a,b)) { return true; } if (!considerWhitespaces && StringUtils.trimToEmpty(a).equals(StringUtils.trimToEmpty(b))) { return true; } if (StringUtils.isBlank(a) && StringUtils.isBlank(b)) { return true; } return false; } private static List<Node> canonizeNodeList(NodeList nodelist) { List<Node> list = new ArrayList<Node>(nodelist.getLength()); for (int i = 0; i < nodelist.getLength(); i++) { Node aItem = nodelist.item(i); if (aItem.getNodeType() == Node.ELEMENT_NODE || aItem.getNodeType() == Node.ATTRIBUTE_NODE) { list.add(aItem); } else if (aItem.getNodeType() == Node.TEXT_NODE || aItem.getNodeType() == Node.CDATA_SECTION_NODE) { if (!aItem.getTextContent().matches("\\s*")) { list.add(aItem); } } } return list; } public static void normalize(Node node, boolean keepWhitespaces) { NodeList childNodes = node.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node aItem = childNodes.item(i); if (aItem.getNodeType() == Node.COMMENT_NODE) { node.removeChild(aItem); i--; } else if (aItem.getNodeType() == Node.TEXT_NODE) { if (aItem.getTextContent().matches("\\s*")) { node.removeChild(aItem); i--; } else { if (!keepWhitespaces) { aItem.setTextContent(aItem.getTextContent().trim()); } } } else if (aItem.getNodeType() == Node.ELEMENT_NODE) { normalize(aItem, keepWhitespaces); } } } public static boolean isJunk(Node node) { if (node.getNodeType() == Node.COMMENT_NODE) { return true; } if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) { return true; } if (node.getNodeType() == Node.TEXT_NODE) { Text text = (Text)node; if (text.getTextContent().matches("^\\s*$")) { return true; } return false; } return false; } public static void validateNonEmptyQName(QName qname, String shortDescription, boolean allowEmptyNamespace) { if (qname == null) { throw new IllegalArgumentException("null" + shortDescription); } // This is hard to enforce. There are situations where unmarshalling // object reference types as ObjectReferenceType, not as prism references. // (E.g. when dealing with reference variables in mappigns/expressions.) // In these cases we need to qualify types after the unmarshalling is complete. if (!allowEmptyNamespace && StringUtils.isEmpty(qname.getNamespaceURI())) { throw new IllegalArgumentException("Missing namespace"+shortDescription); } if (StringUtils.isEmpty(qname.getLocalPart())) { throw new IllegalArgumentException("Missing local part"+shortDescription); } } public static Element findElementRecursive(Element element, QName elementQName) { if (elementQName.equals(getQName(element))) { return element; } for (Element subElement: listChildElements(element)) { Element foundElement = findElementRecursive(subElement, elementQName); if (foundElement != null) { return foundElement; } } return null; } public static QName getQNameWithoutPrefix(Node node) { QName qname = getQName(node); return new QName(qname.getNamespaceURI(), qname.getLocalPart()); } public static boolean isElementName(Element element, QName name) { return name.equals(getQNameWithoutPrefix(element)); } public static boolean isNil(Element element) { String nilString = element.getAttributeNS(XSI_NIL.getNamespaceURI(), XSI_NIL.getLocalPart()); if (nilString == null) { return false; } return Boolean.parseBoolean(nilString); } public static void setNill(Element element) { element.setAttributeNS(XSI_NIL.getNamespaceURI(), XSI_NIL.getLocalPart(), "true"); } /** * Serializes the content of the element to a string (without the eclosing element tags). */ public static String serializeElementContent(Element element) { String completeXml = serializeDOMToString(element); String restXml = completeXml.replaceFirst("^\\s*<[^>]>", ""); return restXml.replaceFirst("</[^>]>\\s*$", ""); } public static boolean isEmpty(Element element) { if (element == null) { return true; } if (hasChildElements(element)) { return false; } if (isNil(element)) { return true; } return StringUtils.isBlank(element.getTextContent()); } public static boolean isEmpty(Attr attr) { if (attr == null) { return true; } return StringUtils.isEmpty(attr.getValue()); } public static void setAttributeValue(Element element, String name, String value) { checkValidXmlChars(value); element.setAttribute(name, value); } public static void setElementTextContent(Element element, String value) { checkValidXmlChars(value); element.setTextContent(value); } public static void checkValidXmlChars(String stringValue) { if (stringValue == null) { return; } for (int i = 0; i < stringValue.length(); i++) { if (!XMLChar.isValid(stringValue.charAt(i))) { throw new IllegalStateException("Invalid character with regards to XML (code " + ((int) stringValue.charAt(i)) + ") in '" + makeSafelyPrintable(stringValue, 200) + "'"); } } } // todo move to some Util class private static String makeSafelyPrintable(String text, int maxSize) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (!XMLChar.isValid(c)) { sb.append('.'); } else if (Character.isWhitespace(c)) { sb.append(' '); } else { sb.append(c); } if (i == maxSize) { sb.append("..."); break; } } return sb.toString(); } public static void createComment(Element element, String text) { if (text != null) { Comment commentNode = element.getOwnerDocument().createComment(replaceInvalidXmlChars(text)); element.appendChild(commentNode); } } private static String replaceInvalidXmlChars(String text) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (!XMLChar.isValid(c)) { sb.append('.'); } else { sb.append(c); } } return sb.toString(); } public static String getAttribute(Element element, QName attrQname) { String attr = element.getAttributeNS(attrQname.getNamespaceURI(), attrQname.getLocalPart()); if (StringUtils.isEmpty(attr)) { // also try without the namespace attr = element.getAttribute(attrQname.getLocalPart()); } return attr; } }