/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.config.spring.dsl.processor.xml; import static org.mule.runtime.config.spring.dsl.model.ApplicationModel.SPRING_CONTEXT_NAMESPACE; import static org.mule.runtime.config.spring.dsl.model.ApplicationModel.SPRING_NAMESPACE; import static org.mule.runtime.config.spring.dsl.processor.xml.XmlCustomAttributeHandler.IS_CDATA; import static org.mule.runtime.config.spring.dsl.processor.xml.XmlCustomAttributeHandler.to; import static org.mule.runtime.internal.dsl.DslConstants.CORE_PREFIX; import static org.mule.runtime.internal.dsl.DslConstants.DOMAIN_NAMESPACE; import static org.mule.runtime.internal.dsl.DslConstants.DOMAIN_PREFIX; import static org.mule.runtime.internal.dsl.DslConstants.EE_DOMAIN_NAMESPACE; import static org.mule.runtime.internal.dsl.DslConstants.EE_DOMAIN_PREFIX; import org.mule.runtime.api.exception.MuleRuntimeException; import org.mule.runtime.config.spring.dsl.processor.ConfigLine; import org.mule.runtime.config.spring.dsl.processor.ConfigLineProvider; import org.mule.runtime.config.spring.parsers.XmlMetadataAnnotations; import org.mule.runtime.core.api.registry.ServiceRegistry; import org.mule.runtime.dsl.api.xml.XmlNamespaceInfo; import org.mule.runtime.dsl.api.xml.XmlNamespaceInfoProvider; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Simple parser that transforms an XML document to a set of {@link org.mule.runtime.config.spring.dsl.processor.ConfigLine} * objects. * <p> * It uses the SPI interface {@link XmlNamespaceInfoProvider} to locate for all namespace info provided and normalize the * namespace from the XML document. * * @since 4.0 */ public class XmlApplicationParser { private static final String COLON = ":"; private static final Map<String, String> predefinedNamespace = new HashMap<>(); private static final String UNDEFINED_NAMESPACE = "undefined"; private final List<XmlNamespaceInfoProvider> namespaceInfoProviders; private final Cache<String, String> namespaceCache; static { predefinedNamespace.put(DOMAIN_NAMESPACE, DOMAIN_PREFIX); predefinedNamespace.put(EE_DOMAIN_NAMESPACE, EE_DOMAIN_PREFIX); predefinedNamespace.put("http://www.springframework.org/schema/beans", SPRING_NAMESPACE); predefinedNamespace.put("http://www.springframework.org/schema/context", SPRING_CONTEXT_NAMESPACE); } public XmlApplicationParser(ServiceRegistry serviceRegistry, List<ClassLoader> pluginsClassLoaders) { final Builder<XmlNamespaceInfoProvider> namespaceInfoProvidersBuilder = ImmutableList.builder(); namespaceInfoProvidersBuilder .addAll(serviceRegistry.lookupProviders(XmlNamespaceInfoProvider.class, XmlNamespaceInfoProvider.class.getClassLoader())); for (ClassLoader pluginClassLoader : pluginsClassLoaders) { namespaceInfoProvidersBuilder.addAll(serviceRegistry.lookupProviders(XmlNamespaceInfoProvider.class, pluginClassLoader)); } namespaceInfoProviders = namespaceInfoProvidersBuilder.build(); namespaceCache = CacheBuilder.newBuilder().build(); } private String loadNamespaceFromProviders(String namespaceUri) { if (predefinedNamespace.containsKey(namespaceUri)) { return predefinedNamespace.get(namespaceUri); } for (XmlNamespaceInfoProvider namespaceInfoProvider : namespaceInfoProviders) { Optional<XmlNamespaceInfo> matchingXmlNamespaceInfo = namespaceInfoProvider.getXmlNamespacesInfo().stream() .filter(xmlNamespaceInfo -> namespaceUri.equals(xmlNamespaceInfo.getNamespaceUriPrefix())).findFirst(); if (matchingXmlNamespaceInfo.isPresent()) { return matchingXmlNamespaceInfo.get().getNamespace(); } } // TODO MULE-9638 for now since just return a fake value since guava cache does not support null values. When done right throw // a configuration exception with a meaningful message if there's no info provider defined return UNDEFINED_NAMESPACE; } public String getNormalizedNamespace(String namespaceUri, String namespacePrefix) { try { return namespaceCache.get(namespaceUri, () -> { String namespace = loadNamespaceFromProviders(namespaceUri); if (namespace == null) { namespace = namespacePrefix; } return namespace; }); } catch (Exception e) { throw new MuleRuntimeException(e); } } public Optional<ConfigLine> parse(Element configElement) { return configLineFromElement(configElement, () -> null); } private Optional<ConfigLine> configLineFromElement(Node node, ConfigLineProvider parentProvider) { if (!isValidType(node)) { return Optional.empty(); } String identifier = parseIdentifier(node); String namespace = parseNamespace(node); ConfigLine.Builder builder = new ConfigLine.Builder().setIdentifier(identifier).setNamespace(namespace).setNode(node).setParent(parentProvider); XmlMetadataAnnotations userData = (XmlMetadataAnnotations) node.getUserData(XmlMetadataAnnotations.METADATA_ANNOTATIONS_KEY); int lineNumber = userData.getLineNumber(); builder.setLineNumber(lineNumber); to(builder).addNode(node); Element element = (Element) node; NamedNodeMap attributes = element.getAttributes(); if (element.hasAttributes()) { for (int i = 0; i < attributes.getLength(); i++) { Node attribute = attributes.item(i); Attr attributeNode = element.getAttributeNode(attribute.getNodeName()); boolean isFromXsd = !attributeNode.getSpecified(); builder.addConfigAttribute(attribute.getNodeName(), attribute.getNodeValue(), isFromXsd); } } if (node.hasChildNodes()) { NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (isTextContent(child)) { builder.setTextContent(child.getNodeValue()); if (child.getNodeType() == Node.CDATA_SECTION_NODE) { builder.addCustomAttribute(IS_CDATA, Boolean.TRUE); break; } } else { configLineFromElement(child, builder::build).ifPresent(builder::addChild); } } } return Optional.of(builder.build()); } private String parseNamespace(Node node) { String namespace = CORE_PREFIX; if (node.getNodeType() != Node.CDATA_SECTION_NODE) { namespace = getNormalizedNamespace(node.getNamespaceURI(), node.getPrefix()); if (namespace.equals(UNDEFINED_NAMESPACE)) { namespace = node.getPrefix(); } } return namespace; } private String parseIdentifier(Node node) { String identifier = node.getNodeName(); String[] nameParts = identifier.split(COLON); if (nameParts.length > 1) { identifier = nameParts[1]; } return identifier; } private boolean isValidType(Node node) { return node.getNodeType() != Node.TEXT_NODE && node.getNodeType() != Node.COMMENT_NODE; } private boolean isTextContent(Node node) { return node.getNodeType() == Node.TEXT_NODE || node.getNodeType() == Node.CDATA_SECTION_NODE; } }