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.ByteArrayInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.StringWriter; 023import java.nio.charset.StandardCharsets; 024import java.util.Iterator; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Properties; 029import java.util.concurrent.atomic.AtomicBoolean; 030 031import javax.xml.bind.Binder; 032import javax.xml.bind.JAXBContext; 033import javax.xml.bind.JAXBException; 034import javax.xml.bind.Marshaller; 035import javax.xml.bind.Unmarshaller; 036import javax.xml.transform.OutputKeys; 037import javax.xml.transform.TransformerException; 038 039import org.w3c.dom.Document; 040import org.w3c.dom.Element; 041import org.w3c.dom.NamedNodeMap; 042import org.w3c.dom.Node; 043 044import org.apache.camel.CamelContext; 045import org.apache.camel.DelegateEndpoint; 046import org.apache.camel.Endpoint; 047import org.apache.camel.Expression; 048import org.apache.camel.ExtendedCamelContext; 049import org.apache.camel.NamedNode; 050import org.apache.camel.TypeConversionException; 051import org.apache.camel.converter.jaxp.XmlConverter; 052import org.apache.camel.model.language.ExpressionDefinition; 053import org.apache.camel.model.rest.RestDefinition; 054import org.apache.camel.model.rest.RestsDefinition; 055import org.apache.camel.spi.ModelJAXBContextFactory; 056import org.apache.camel.spi.NamespaceAware; 057import org.apache.camel.spi.TypeConverterRegistry; 058import org.apache.camel.util.ObjectHelper; 059import org.apache.camel.util.xml.XmlLineNumberParser; 060 061import static org.apache.camel.model.ProcessorDefinitionHelper.filterTypeInOutputs; 062 063/** 064 * Helper for the Camel {@link org.apache.camel.model model} classes. 065 */ 066public final class ModelHelper { 067 068 private ModelHelper() { 069 // utility class 070 } 071 072 /** 073 * Dumps the definition as XML 074 * 075 * @param context the CamelContext, if <tt>null</tt> then 076 * {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in 077 * use 078 * @param definition the definition, such as a 079 * {@link org.apache.camel.NamedNode} 080 * @return the output in XML (is formatted) 081 * @throws JAXBException is throw if error marshalling to XML 082 */ 083 public static String dumpModelAsXml(CamelContext context, NamedNode definition) throws JAXBException { 084 JAXBContext jaxbContext = getJAXBContext(context); 085 final Map<String, String> namespaces = new LinkedHashMap<>(); 086 087 // gather all namespaces from the routes or route which is stored on the 088 // expression nodes 089 if (definition instanceof RoutesDefinition) { 090 List<RouteDefinition> routes = ((RoutesDefinition)definition).getRoutes(); 091 for (RouteDefinition route : routes) { 092 extractNamespaces(route, namespaces); 093 } 094 } else if (definition instanceof RouteDefinition) { 095 RouteDefinition route = (RouteDefinition)definition; 096 extractNamespaces(route, namespaces); 097 } 098 099 Marshaller marshaller = jaxbContext.createMarshaller(); 100 marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); 101 marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); 102 StringWriter buffer = new StringWriter(); 103 marshaller.marshal(definition, buffer); 104 105 XmlConverter xmlConverter = newXmlConverter(context); 106 String xml = buffer.toString(); 107 Document dom; 108 try { 109 dom = xmlConverter.toDOMDocument(xml, null); 110 } catch (Exception e) { 111 throw new TypeConversionException(xml, Document.class, e); 112 } 113 114 // Add additional namespaces to the document root element 115 Element documentElement = dom.getDocumentElement(); 116 for (String nsPrefix : namespaces.keySet()) { 117 String prefix = nsPrefix.equals("xmlns") ? nsPrefix : "xmlns:" + nsPrefix; 118 documentElement.setAttribute(prefix, namespaces.get(nsPrefix)); 119 } 120 121 // We invoke the type converter directly because we need to pass some 122 // custom XML output options 123 Properties outputProperties = new Properties(); 124 outputProperties.put(OutputKeys.INDENT, "yes"); 125 outputProperties.put(OutputKeys.STANDALONE, "yes"); 126 outputProperties.put(OutputKeys.ENCODING, "UTF-8"); 127 try { 128 return xmlConverter.toStringFromDocument(dom, outputProperties); 129 } catch (TransformerException e) { 130 throw new IllegalStateException("Failed converting document object to string", e); 131 } 132 } 133 134 /** 135 * Dumps the definition as XML 136 * 137 * @param context the CamelContext, if <tt>null</tt> then 138 * {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in 139 * use 140 * @param definition the definition, such as a 141 * {@link org.apache.camel.NamedNode} 142 * @param resolvePlaceholders whether to resolve property placeholders in 143 * the dumped XML 144 * @param resolveDelegateEndpoints whether to resolve delegate endpoints in 145 * the dumped XML (limited to endpoints used in uri attributes in 146 * the model) 147 * @return the output in XML (is formatted) 148 * @throws Exception is throw if error marshalling to XML 149 */ 150 public static String dumpModelAsXml(CamelContext context, NamedNode definition, boolean resolvePlaceholders, boolean resolveDelegateEndpoints) throws Exception { 151 String xml = ModelHelper.dumpModelAsXml(context, definition); 152 153 // if resolving placeholders we parse the xml, and resolve the property 154 // placeholders during parsing 155 if (resolvePlaceholders || resolveDelegateEndpoints) { 156 final AtomicBoolean changed = new AtomicBoolean(); 157 InputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); 158 Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() { 159 160 private String prev; 161 162 @Override 163 public String transform(String text) { 164 String after = text; 165 if (resolveDelegateEndpoints && "uri".equals(prev)) { 166 try { 167 // must resolve placeholder as the endpoint may use 168 // property placeholders 169 String uri = context.resolvePropertyPlaceholders(text); 170 Endpoint endpoint = context.hasEndpoint(uri); 171 if (endpoint instanceof DelegateEndpoint) { 172 endpoint = ((DelegateEndpoint)endpoint).getEndpoint(); 173 after = endpoint.getEndpointUri(); 174 } 175 } catch (Exception e) { 176 // ignore 177 } 178 } 179 180 if (resolvePlaceholders) { 181 try { 182 after = context.resolvePropertyPlaceholders(after); 183 } catch (Exception e) { 184 // ignore 185 } 186 } 187 188 if (!changed.get()) { 189 changed.set(!text.equals(after)); 190 } 191 192 // okay the previous must be the attribute key with uri, so 193 // it refers to an endpoint 194 prev = text; 195 196 return after; 197 } 198 }); 199 200 // okay there were some property placeholder or delegate endpoints 201 // replaced so re-create the model 202 if (changed.get()) { 203 xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom); 204 NamedNode copy = ModelHelper.createModelFromXml(context, xml, NamedNode.class); 205 xml = ModelHelper.dumpModelAsXml(context, copy); 206 } 207 } 208 209 return xml; 210 } 211 212 /** 213 * Marshal the xml to the model definition 214 * 215 * @param context the CamelContext, if <tt>null</tt> then 216 * {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in 217 * use 218 * @param xml the xml 219 * @param type the definition type to return, will throw a 220 * {@link ClassCastException} if not the expected type 221 * @return the model definition 222 * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling 223 * from xml to model 224 */ 225 public static <T extends NamedNode> T createModelFromXml(CamelContext context, String xml, Class<T> type) throws JAXBException { 226 return modelToXml(context, null, xml, type); 227 } 228 229 /** 230 * Marshal the xml to the model definition 231 * 232 * @param context the CamelContext, if <tt>null</tt> then 233 * {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in 234 * use 235 * @param stream the xml stream 236 * @param type the definition type to return, will throw a 237 * {@link ClassCastException} if not the expected type 238 * @return the model definition 239 * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling 240 * from xml to model 241 */ 242 public static <T extends NamedNode> T createModelFromXml(CamelContext context, InputStream stream, Class<T> type) throws JAXBException { 243 return modelToXml(context, stream, null, type); 244 } 245 246 /** 247 * Marshal the xml to the model definition 248 * 249 * @param context the CamelContext, if <tt>null</tt> then 250 * {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in 251 * use 252 * @param inputStream the xml stream 253 * @throws Exception is thrown if an error is encountered unmarshalling from 254 * xml to model 255 */ 256 public static RoutesDefinition loadRoutesDefinition(CamelContext context, InputStream inputStream) throws Exception { 257 XmlConverter xmlConverter = newXmlConverter(context); 258 Document dom = xmlConverter.toDOMDocument(inputStream, null); 259 return loadRoutesDefinition(context, dom); 260 } 261 262 /** 263 * Marshal the xml to the model definition 264 * 265 * @param context the CamelContext, if <tt>null</tt> then 266 * {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in 267 * use 268 * @param node the xml node 269 * @throws Exception is thrown if an error is encountered unmarshalling from 270 * xml to model 271 */ 272 public static RoutesDefinition loadRoutesDefinition(CamelContext context, Node node) throws Exception { 273 JAXBContext jaxbContext = getJAXBContext(context); 274 275 Map<String, String> namespaces = new LinkedHashMap<>(); 276 277 Document dom = node instanceof Document ? (Document)node : node.getOwnerDocument(); 278 extractNamespaces(dom, namespaces); 279 280 Binder<Node> binder = jaxbContext.createBinder(); 281 Object result = binder.unmarshal(node); 282 283 if (result == null) { 284 throw new JAXBException("Cannot unmarshal to RoutesDefinition using JAXB"); 285 } 286 287 // can either be routes or a single route 288 RoutesDefinition answer; 289 if (result instanceof RouteDefinition) { 290 RouteDefinition route = (RouteDefinition)result; 291 answer = new RoutesDefinition(); 292 applyNamespaces(route, namespaces); 293 answer.getRoutes().add(route); 294 } else if (result instanceof RoutesDefinition) { 295 answer = (RoutesDefinition)result; 296 for (RouteDefinition route : answer.getRoutes()) { 297 applyNamespaces(route, namespaces); 298 } 299 } else { 300 throw new IllegalArgumentException("Unmarshalled object is an unsupported type: " + ObjectHelper.className(result) + " -> " + result); 301 } 302 303 return answer; 304 } 305 306 public static RestsDefinition loadRestsDefinition(CamelContext context, InputStream is) throws Exception { 307 // load routes using JAXB 308 Unmarshaller unmarshaller = getJAXBContext(context).createUnmarshaller(); 309 Object result = unmarshaller.unmarshal(is); 310 311 if (result == null) { 312 throw new IOException("Cannot unmarshal to rests using JAXB from input stream: " + is); 313 } 314 315 // can either be routes or a single route 316 RestsDefinition answer; 317 if (result instanceof RestDefinition) { 318 RestDefinition rest = (RestDefinition)result; 319 answer = new RestsDefinition(); 320 answer.getRests().add(rest); 321 } else if (result instanceof RestsDefinition) { 322 answer = (RestsDefinition)result; 323 } else { 324 throw new IllegalArgumentException("Unmarshalled object is an unsupported type: " + ObjectHelper.className(result) + " -> " + result); 325 } 326 327 return answer; 328 } 329 330 private static <T extends NamedNode> T modelToXml(CamelContext context, InputStream is, String xml, Class<T> type) throws JAXBException { 331 JAXBContext jaxbContext = getJAXBContext(context); 332 333 XmlConverter xmlConverter = newXmlConverter(context); 334 Document dom = null; 335 try { 336 if (is != null) { 337 dom = xmlConverter.toDOMDocument(is, null); 338 } else if (xml != null) { 339 dom = xmlConverter.toDOMDocument(xml, null); 340 } 341 } catch (Exception e) { 342 throw new TypeConversionException(xml, Document.class, e); 343 } 344 if (dom == null) { 345 throw new IllegalArgumentException("InputStream and XML is both null"); 346 } 347 348 Map<String, String> namespaces = new LinkedHashMap<>(); 349 extractNamespaces(dom, namespaces); 350 351 Binder<Node> binder = jaxbContext.createBinder(); 352 Object result = binder.unmarshal(dom); 353 354 if (result == null) { 355 throw new JAXBException("Cannot unmarshal to " + type + " using JAXB"); 356 } 357 358 // Restore namespaces to anything that's NamespaceAware 359 if (result instanceof RoutesDefinition) { 360 List<RouteDefinition> routes = ((RoutesDefinition)result).getRoutes(); 361 for (RouteDefinition route : routes) { 362 applyNamespaces(route, namespaces); 363 } 364 } else if (result instanceof RouteDefinition) { 365 RouteDefinition route = (RouteDefinition)result; 366 applyNamespaces(route, namespaces); 367 } 368 369 return type.cast(result); 370 } 371 372 private static JAXBContext getJAXBContext(CamelContext context) throws JAXBException { 373 ModelJAXBContextFactory factory = context.adapt(ExtendedCamelContext.class).getModelJAXBContextFactory(); 374 return factory.newJAXBContext(); 375 } 376 377 private static void applyNamespaces(RouteDefinition route, Map<String, String> namespaces) { 378 Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class); 379 while (it.hasNext()) { 380 NamespaceAware na = getNamespaceAwareFromExpression(it.next()); 381 if (na != null) { 382 na.setNamespaces(namespaces); 383 } 384 } 385 } 386 387 private static NamespaceAware getNamespaceAwareFromExpression(ExpressionNode expressionNode) { 388 ExpressionDefinition ed = expressionNode.getExpression(); 389 390 NamespaceAware na = null; 391 Expression exp = ed.getExpressionValue(); 392 if (exp instanceof NamespaceAware) { 393 na = (NamespaceAware)exp; 394 } else if (ed instanceof NamespaceAware) { 395 na = (NamespaceAware)ed; 396 } 397 398 return na; 399 } 400 401 /** 402 * Extract all XML namespaces from the expressions in the route 403 * 404 * @param route the route 405 * @param namespaces the map of namespaces to add discovered XML namespaces 406 * into 407 */ 408 private static void extractNamespaces(RouteDefinition route, Map<String, String> namespaces) { 409 Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class); 410 while (it.hasNext()) { 411 NamespaceAware na = getNamespaceAwareFromExpression(it.next()); 412 413 if (na != null) { 414 Map<String, String> map = na.getNamespaces(); 415 if (map != null && !map.isEmpty()) { 416 namespaces.putAll(map); 417 } 418 } 419 } 420 } 421 422 /** 423 * Extract all XML namespaces from the root element in a DOM Document 424 * 425 * @param document the DOM document 426 * @param namespaces the map of namespaces to add new found XML namespaces 427 */ 428 private static void extractNamespaces(Document document, Map<String, String> namespaces) throws JAXBException { 429 NamedNodeMap attributes = document.getDocumentElement().getAttributes(); 430 for (int i = 0; i < attributes.getLength(); i++) { 431 Node item = attributes.item(i); 432 String nsPrefix = item.getNodeName(); 433 if (nsPrefix != null && nsPrefix.startsWith("xmlns")) { 434 String nsValue = item.getNodeValue(); 435 String[] nsParts = nsPrefix.split(":"); 436 if (nsParts.length == 1) { 437 namespaces.put(nsParts[0], nsValue); 438 } else if (nsParts.length == 2) { 439 namespaces.put(nsParts[1], nsValue); 440 } else { 441 // Fallback on adding the namespace prefix as we find it 442 namespaces.put(nsPrefix, nsValue); 443 } 444 } 445 } 446 } 447 448 /** 449 * Creates a new {@link XmlConverter} 450 * 451 * @param context CamelContext if provided 452 * @return a new XmlConverter instance 453 */ 454 private static XmlConverter newXmlConverter(CamelContext context) { 455 XmlConverter xmlConverter; 456 if (context != null) { 457 TypeConverterRegistry registry = context.getTypeConverterRegistry(); 458 xmlConverter = registry.getInjector().newInstance(XmlConverter.class, false); 459 } else { 460 xmlConverter = new XmlConverter(); 461 } 462 return xmlConverter; 463 } 464 465}