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}