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