/** * 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.camel.model; import java.io.InputStream; import java.io.StringWriter; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import javax.xml.bind.Binder; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.transform.OutputKeys; import javax.xml.transform.TransformerException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.apache.camel.CamelContext; import org.apache.camel.Expression; import org.apache.camel.NamedNode; import org.apache.camel.TypeConversionException; import org.apache.camel.converter.jaxp.XmlConverter; import org.apache.camel.model.language.ExpressionDefinition; import org.apache.camel.spi.NamespaceAware; import org.apache.camel.spi.TypeConverterRegistry; import org.apache.camel.util.ObjectHelper; import static org.apache.camel.model.ProcessorDefinitionHelper.filterTypeInOutputs; /** * Helper for the Camel {@link org.apache.camel.model model} classes. */ public final class ModelHelper { private ModelHelper() { // utility class } /** * Dumps the definition as XML * * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use * @param definition the definition, such as a {@link org.apache.camel.NamedNode} * @return the output in XML (is formatted) * @throws JAXBException is throw if error marshalling to XML */ public static String dumpModelAsXml(CamelContext context, NamedNode definition) throws JAXBException { JAXBContext jaxbContext = getJAXBContext(context); final Map<String, String> namespaces = new LinkedHashMap<>(); // gather all namespaces from the routes or route which is stored on the expression nodes if (definition instanceof RoutesDefinition) { List<RouteDefinition> routes = ((RoutesDefinition) definition).getRoutes(); for (RouteDefinition route : routes) { extractNamespaces(route, namespaces); } } else if (definition instanceof RouteDefinition) { RouteDefinition route = (RouteDefinition) definition; extractNamespaces(route, namespaces); } Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); StringWriter buffer = new StringWriter(); marshaller.marshal(definition, buffer); XmlConverter xmlConverter = newXmlConverter(context); String xml = buffer.toString(); Document dom; try { dom = xmlConverter.toDOMDocument(xml, null); } catch (Exception e) { throw new TypeConversionException(xml, Document.class, e); } // Add additional namespaces to the document root element Element documentElement = dom.getDocumentElement(); for (String nsPrefix : namespaces.keySet()) { String prefix = nsPrefix.equals("xmlns") ? nsPrefix : "xmlns:" + nsPrefix; documentElement.setAttribute(prefix, namespaces.get(nsPrefix)); } // We invoke the type converter directly because we need to pass some custom XML output options Properties outputProperties = new Properties(); outputProperties.put(OutputKeys.INDENT, "yes"); outputProperties.put(OutputKeys.STANDALONE, "yes"); try { return xmlConverter.toStringFromDocument(dom, outputProperties); } catch (TransformerException e) { throw new IllegalStateException("Failed converting document object to string", e); } } /** * Marshal the xml to the model definition * * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use * @param xml the xml * @param type the definition type to return, will throw a {@link ClassCastException} if not the expected type * @return the model definition * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling from xml to model */ public static <T extends NamedNode> T createModelFromXml(CamelContext context, String xml, Class<T> type) throws JAXBException { return modelToXml(context, null, xml, type); } /** * Marshal the xml to the model definition * * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use * @param stream the xml stream * @param type the definition type to return, will throw a {@link ClassCastException} if not the expected type * @return the model definition * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling from xml to model */ public static <T extends NamedNode> T createModelFromXml(CamelContext context, InputStream stream, Class<T> type) throws JAXBException { return modelToXml(context, stream, null, type); } /** * Marshal the xml to the model definition * * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use * @param inputStream the xml stream * @throws Exception is thrown if an error is encountered unmarshalling from xml to model */ public static RoutesDefinition loadRoutesDefinition(CamelContext context, InputStream inputStream) throws Exception { XmlConverter xmlConverter = newXmlConverter(context); Document dom = xmlConverter.toDOMDocument(inputStream, null); return loadRoutesDefinition(context, dom); } /** * Marshal the xml to the model definition * * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use * @param node the xml node * @throws Exception is thrown if an error is encountered unmarshalling from xml to model */ public static RoutesDefinition loadRoutesDefinition(CamelContext context, Node node) throws Exception { JAXBContext jaxbContext = getJAXBContext(context); Map<String, String> namespaces = new LinkedHashMap<>(); Document dom = node instanceof Document ? (Document) node : node.getOwnerDocument(); extractNamespaces(dom, namespaces); Binder<Node> binder = jaxbContext.createBinder(); Object result = binder.unmarshal(node); if (result == null) { throw new JAXBException("Cannot unmarshal to RoutesDefinition using JAXB"); } // can either be routes or a single route RoutesDefinition answer; if (result instanceof RouteDefinition) { RouteDefinition route = (RouteDefinition) result; answer = new RoutesDefinition(); applyNamespaces(route, namespaces); answer.getRoutes().add(route); } else if (result instanceof RoutesDefinition) { answer = (RoutesDefinition) result; for (RouteDefinition route : answer.getRoutes()) { applyNamespaces(route, namespaces); } } else { throw new IllegalArgumentException("Unmarshalled object is an unsupported type: " + ObjectHelper.className(result) + " -> " + result); } return answer; } private static <T extends NamedNode> T modelToXml(CamelContext context, InputStream is, String xml, Class<T> type) throws JAXBException { JAXBContext jaxbContext = getJAXBContext(context); XmlConverter xmlConverter = newXmlConverter(context); Document dom = null; try { if (is != null) { dom = xmlConverter.toDOMDocument(is, null); } else if (xml != null) { dom = xmlConverter.toDOMDocument(xml, null); } } catch (Exception e) { throw new TypeConversionException(xml, Document.class, e); } if (dom == null) { throw new IllegalArgumentException("InputStream and XML is both null"); } Map<String, String> namespaces = new LinkedHashMap<>(); extractNamespaces(dom, namespaces); Binder<Node> binder = jaxbContext.createBinder(); Object result = binder.unmarshal(dom); if (result == null) { throw new JAXBException("Cannot unmarshal to " + type + " using JAXB"); } // Restore namespaces to anything that's NamespaceAware if (result instanceof RoutesDefinition) { List<RouteDefinition> routes = ((RoutesDefinition) result).getRoutes(); for (RouteDefinition route : routes) { applyNamespaces(route, namespaces); } } else if (result instanceof RouteDefinition) { RouteDefinition route = (RouteDefinition) result; applyNamespaces(route, namespaces); } return type.cast(result); } private static JAXBContext getJAXBContext(CamelContext context) throws JAXBException { JAXBContext jaxbContext; if (context == null) { jaxbContext = createJAXBContext(); } else { jaxbContext = context.getModelJAXBContextFactory().newJAXBContext(); } return jaxbContext; } private static void applyNamespaces(RouteDefinition route, Map<String, String> namespaces) { Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class); while (it.hasNext()) { NamespaceAware na = getNamespaceAwareFromExpression(it.next()); if (na != null) { na.setNamespaces(namespaces); } } } private static NamespaceAware getNamespaceAwareFromExpression(ExpressionNode expressionNode) { ExpressionDefinition ed = expressionNode.getExpression(); NamespaceAware na = null; Expression exp = ed.getExpressionValue(); if (exp instanceof NamespaceAware) { na = (NamespaceAware) exp; } else if (ed instanceof NamespaceAware) { na = (NamespaceAware) ed; } return na; } private static JAXBContext createJAXBContext() throws JAXBException { // must use classloader from CamelContext to have JAXB working return JAXBContext.newInstance(Constants.JAXB_CONTEXT_PACKAGES, CamelContext.class.getClassLoader()); } /** * Extract all XML namespaces from the expressions in the route * * @param route the route * @param namespaces the map of namespaces to add discovered XML namespaces into */ private static void extractNamespaces(RouteDefinition route, Map<String, String> namespaces) { Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class); while (it.hasNext()) { NamespaceAware na = getNamespaceAwareFromExpression(it.next()); if (na != null) { Map<String, String> map = na.getNamespaces(); if (map != null && !map.isEmpty()) { namespaces.putAll(map); } } } } /** * Extract all XML namespaces from the root element in a DOM Document * * @param document the DOM document * @param namespaces the map of namespaces to add new found XML namespaces */ private static void extractNamespaces(Document document, Map<String, String> namespaces) throws JAXBException { NamedNodeMap attributes = document.getDocumentElement().getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Node item = attributes.item(i); String nsPrefix = item.getNodeName(); if (nsPrefix != null && nsPrefix.startsWith("xmlns")) { String nsValue = item.getNodeValue(); String[] nsParts = nsPrefix.split(":"); if (nsParts.length == 1) { namespaces.put(nsParts[0], nsValue); } else if (nsParts.length == 2) { namespaces.put(nsParts[1], nsValue); } else { // Fallback on adding the namespace prefix as we find it namespaces.put(nsPrefix, nsValue); } } } } /** * Creates a new {@link XmlConverter} * * @param context CamelContext if provided * @return a new XmlConverter instance */ private static XmlConverter newXmlConverter(CamelContext context) { XmlConverter xmlConverter; if (context != null) { TypeConverterRegistry registry = context.getTypeConverterRegistry(); xmlConverter = registry.getInjector().newInstance(XmlConverter.class); } else { xmlConverter = new XmlConverter(); } return xmlConverter; } }