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