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}