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}