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