001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.model; 018 019import java.io.InputStream; 020import java.io.StringWriter; 021import java.util.Iterator; 022import java.util.LinkedHashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.Properties; 026import javax.xml.bind.Binder; 027import javax.xml.bind.JAXBContext; 028import javax.xml.bind.JAXBException; 029import javax.xml.bind.Marshaller; 030import javax.xml.transform.OutputKeys; 031import javax.xml.transform.TransformerException; 032 033import org.w3c.dom.Document; 034import org.w3c.dom.Element; 035import org.w3c.dom.NamedNodeMap; 036import org.w3c.dom.Node; 037 038import org.apache.camel.CamelContext; 039import org.apache.camel.Expression; 040import org.apache.camel.NamedNode; 041import org.apache.camel.TypeConversionException; 042import org.apache.camel.converter.jaxp.XmlConverter; 043import org.apache.camel.model.language.ExpressionDefinition; 044import org.apache.camel.spi.NamespaceAware; 045import org.apache.camel.spi.TypeConverterRegistry; 046import org.apache.camel.util.ObjectHelper; 047 048import static org.apache.camel.model.ProcessorDefinitionHelper.filterTypeInOutputs; 049 050/** 051 * Helper for the Camel {@link org.apache.camel.model model} classes. 052 */ 053public final class ModelHelper { 054 055 private ModelHelper() { 056 // utility class 057 } 058 059 /** 060 * Dumps the definition as XML 061 * 062 * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use 063 * @param definition the definition, such as a {@link org.apache.camel.NamedNode} 064 * @return the output in XML (is formatted) 065 * @throws JAXBException is throw if error marshalling to XML 066 */ 067 public static String dumpModelAsXml(CamelContext context, NamedNode definition) throws JAXBException { 068 JAXBContext jaxbContext = getJAXBContext(context); 069 final Map<String, String> namespaces = new LinkedHashMap<>(); 070 071 // gather all namespaces from the routes or route which is stored on the expression nodes 072 if (definition instanceof RoutesDefinition) { 073 List<RouteDefinition> routes = ((RoutesDefinition) definition).getRoutes(); 074 for (RouteDefinition route : routes) { 075 extractNamespaces(route, namespaces); 076 } 077 } else if (definition instanceof RouteDefinition) { 078 RouteDefinition route = (RouteDefinition) definition; 079 extractNamespaces(route, namespaces); 080 } 081 082 Marshaller marshaller = jaxbContext.createMarshaller(); 083 marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); 084 marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); 085 StringWriter buffer = new StringWriter(); 086 marshaller.marshal(definition, buffer); 087 088 XmlConverter xmlConverter = newXmlConverter(context); 089 String xml = buffer.toString(); 090 Document dom; 091 try { 092 dom = xmlConverter.toDOMDocument(xml, null); 093 } catch (Exception e) { 094 throw new TypeConversionException(xml, Document.class, e); 095 } 096 097 // Add additional namespaces to the document root element 098 Element documentElement = dom.getDocumentElement(); 099 for (String nsPrefix : namespaces.keySet()) { 100 String prefix = nsPrefix.equals("xmlns") ? nsPrefix : "xmlns:" + nsPrefix; 101 documentElement.setAttribute(prefix, namespaces.get(nsPrefix)); 102 } 103 104 // We invoke the type converter directly because we need to pass some custom XML output options 105 Properties outputProperties = new Properties(); 106 outputProperties.put(OutputKeys.INDENT, "yes"); 107 outputProperties.put(OutputKeys.STANDALONE, "yes"); 108 outputProperties.put(OutputKeys.ENCODING, "UTF-8"); 109 try { 110 return xmlConverter.toStringFromDocument(dom, outputProperties); 111 } catch (TransformerException e) { 112 throw new IllegalStateException("Failed converting document object to string", e); 113 } 114 } 115 116 /** 117 * Marshal the xml to the model definition 118 * 119 * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use 120 * @param xml the xml 121 * @param type the definition type to return, will throw a {@link ClassCastException} if not the expected type 122 * @return the model definition 123 * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling from xml to model 124 */ 125 public static <T extends NamedNode> T createModelFromXml(CamelContext context, String xml, Class<T> type) throws JAXBException { 126 return modelToXml(context, null, xml, type); 127 } 128 129 /** 130 * Marshal the xml to the model definition 131 * 132 * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use 133 * @param stream the xml stream 134 * @param type the definition type to return, will throw a {@link ClassCastException} if not the expected type 135 * @return the model definition 136 * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling from xml to model 137 */ 138 public static <T extends NamedNode> T createModelFromXml(CamelContext context, InputStream stream, Class<T> type) throws JAXBException { 139 return modelToXml(context, stream, null, type); 140 } 141 142 /** 143 * Marshal the xml to the model definition 144 * 145 * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use 146 * @param inputStream the xml stream 147 * @throws Exception is thrown if an error is encountered unmarshalling from xml to model 148 */ 149 public static RoutesDefinition loadRoutesDefinition(CamelContext context, InputStream inputStream) throws Exception { 150 XmlConverter xmlConverter = newXmlConverter(context); 151 Document dom = xmlConverter.toDOMDocument(inputStream, null); 152 return loadRoutesDefinition(context, dom); 153 } 154 155 /** 156 * Marshal the xml to the model definition 157 * 158 * @param context the CamelContext, if <tt>null</tt> then {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in use 159 * @param node the xml node 160 * @throws Exception is thrown if an error is encountered unmarshalling from xml to model 161 */ 162 public static RoutesDefinition loadRoutesDefinition(CamelContext context, Node node) throws Exception { 163 JAXBContext jaxbContext = getJAXBContext(context); 164 165 Map<String, String> namespaces = new LinkedHashMap<>(); 166 167 Document dom = node instanceof Document ? (Document) node : node.getOwnerDocument(); 168 extractNamespaces(dom, namespaces); 169 170 Binder<Node> binder = jaxbContext.createBinder(); 171 Object result = binder.unmarshal(node); 172 173 if (result == null) { 174 throw new JAXBException("Cannot unmarshal to RoutesDefinition using JAXB"); 175 } 176 177 // can either be routes or a single route 178 RoutesDefinition answer; 179 if (result instanceof RouteDefinition) { 180 RouteDefinition route = (RouteDefinition) result; 181 answer = new RoutesDefinition(); 182 applyNamespaces(route, namespaces); 183 answer.getRoutes().add(route); 184 } else if (result instanceof RoutesDefinition) { 185 answer = (RoutesDefinition) result; 186 for (RouteDefinition route : answer.getRoutes()) { 187 applyNamespaces(route, namespaces); 188 } 189 } else { 190 throw new IllegalArgumentException("Unmarshalled object is an unsupported type: " + ObjectHelper.className(result) + " -> " + result); 191 } 192 193 return answer; 194 } 195 196 private static <T extends NamedNode> T modelToXml(CamelContext context, InputStream is, String xml, Class<T> type) throws JAXBException { 197 JAXBContext jaxbContext = getJAXBContext(context); 198 199 XmlConverter xmlConverter = newXmlConverter(context); 200 Document dom = null; 201 try { 202 if (is != null) { 203 dom = xmlConverter.toDOMDocument(is, null); 204 } else if (xml != null) { 205 dom = xmlConverter.toDOMDocument(xml, null); 206 } 207 } catch (Exception e) { 208 throw new TypeConversionException(xml, Document.class, e); 209 } 210 if (dom == null) { 211 throw new IllegalArgumentException("InputStream and XML is both null"); 212 } 213 214 Map<String, String> namespaces = new LinkedHashMap<>(); 215 extractNamespaces(dom, namespaces); 216 217 Binder<Node> binder = jaxbContext.createBinder(); 218 Object result = binder.unmarshal(dom); 219 220 if (result == null) { 221 throw new JAXBException("Cannot unmarshal to " + type + " using JAXB"); 222 } 223 224 // Restore namespaces to anything that's NamespaceAware 225 if (result instanceof RoutesDefinition) { 226 List<RouteDefinition> routes = ((RoutesDefinition) result).getRoutes(); 227 for (RouteDefinition route : routes) { 228 applyNamespaces(route, namespaces); 229 } 230 } else if (result instanceof RouteDefinition) { 231 RouteDefinition route = (RouteDefinition) result; 232 applyNamespaces(route, namespaces); 233 } 234 235 return type.cast(result); 236 } 237 238 private static JAXBContext getJAXBContext(CamelContext context) throws JAXBException { 239 JAXBContext jaxbContext; 240 if (context == null) { 241 jaxbContext = createJAXBContext(); 242 } else { 243 jaxbContext = context.getModelJAXBContextFactory().newJAXBContext(); 244 } 245 return jaxbContext; 246 } 247 248 private static void applyNamespaces(RouteDefinition route, Map<String, String> namespaces) { 249 Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class); 250 while (it.hasNext()) { 251 NamespaceAware na = getNamespaceAwareFromExpression(it.next()); 252 if (na != null) { 253 na.setNamespaces(namespaces); 254 } 255 } 256 } 257 258 private static NamespaceAware getNamespaceAwareFromExpression(ExpressionNode expressionNode) { 259 ExpressionDefinition ed = expressionNode.getExpression(); 260 261 NamespaceAware na = null; 262 Expression exp = ed.getExpressionValue(); 263 if (exp instanceof NamespaceAware) { 264 na = (NamespaceAware) exp; 265 } else if (ed instanceof NamespaceAware) { 266 na = (NamespaceAware) ed; 267 } 268 269 return na; 270 } 271 272 private static JAXBContext createJAXBContext() throws JAXBException { 273 // must use classloader from CamelContext to have JAXB working 274 return JAXBContext.newInstance(Constants.JAXB_CONTEXT_PACKAGES, CamelContext.class.getClassLoader()); 275 } 276 277 /** 278 * Extract all XML namespaces from the expressions in the route 279 * 280 * @param route the route 281 * @param namespaces the map of namespaces to add discovered XML namespaces into 282 */ 283 private static void extractNamespaces(RouteDefinition route, Map<String, String> namespaces) { 284 Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class); 285 while (it.hasNext()) { 286 NamespaceAware na = getNamespaceAwareFromExpression(it.next()); 287 288 if (na != null) { 289 Map<String, String> map = na.getNamespaces(); 290 if (map != null && !map.isEmpty()) { 291 namespaces.putAll(map); 292 } 293 } 294 } 295 } 296 297 /** 298 * Extract all XML namespaces from the root element in a DOM Document 299 * 300 * @param document the DOM document 301 * @param namespaces the map of namespaces to add new found XML namespaces 302 */ 303 private static void extractNamespaces(Document document, Map<String, String> namespaces) throws JAXBException { 304 NamedNodeMap attributes = document.getDocumentElement().getAttributes(); 305 for (int i = 0; i < attributes.getLength(); i++) { 306 Node item = attributes.item(i); 307 String nsPrefix = item.getNodeName(); 308 if (nsPrefix != null && nsPrefix.startsWith("xmlns")) { 309 String nsValue = item.getNodeValue(); 310 String[] nsParts = nsPrefix.split(":"); 311 if (nsParts.length == 1) { 312 namespaces.put(nsParts[0], nsValue); 313 } else if (nsParts.length == 2) { 314 namespaces.put(nsParts[1], nsValue); 315 } else { 316 // Fallback on adding the namespace prefix as we find it 317 namespaces.put(nsPrefix, nsValue); 318 } 319 } 320 } 321 } 322 323 /** 324 * Creates a new {@link XmlConverter} 325 * 326 * @param context CamelContext if provided 327 * @return a new XmlConverter instance 328 */ 329 private static XmlConverter newXmlConverter(CamelContext context) { 330 XmlConverter xmlConverter; 331 if (context != null) { 332 TypeConverterRegistry registry = context.getTypeConverterRegistry(); 333 xmlConverter = registry.getInjector().newInstance(XmlConverter.class); 334 } else { 335 xmlConverter = new XmlConverter(); 336 } 337 return xmlConverter; 338 } 339 340}