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