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.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.StringWriter;
023import java.nio.charset.StandardCharsets;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029import java.util.concurrent.atomic.AtomicBoolean;
030
031import javax.xml.bind.Binder;
032import javax.xml.bind.JAXBContext;
033import javax.xml.bind.JAXBException;
034import javax.xml.bind.Marshaller;
035import javax.xml.bind.Unmarshaller;
036import javax.xml.transform.OutputKeys;
037import javax.xml.transform.TransformerException;
038
039import org.w3c.dom.Document;
040import org.w3c.dom.Element;
041import org.w3c.dom.NamedNodeMap;
042import org.w3c.dom.Node;
043
044import org.apache.camel.CamelContext;
045import org.apache.camel.DelegateEndpoint;
046import org.apache.camel.Endpoint;
047import org.apache.camel.Expression;
048import org.apache.camel.ExtendedCamelContext;
049import org.apache.camel.NamedNode;
050import org.apache.camel.TypeConversionException;
051import org.apache.camel.converter.jaxp.XmlConverter;
052import org.apache.camel.model.language.ExpressionDefinition;
053import org.apache.camel.model.rest.RestDefinition;
054import org.apache.camel.model.rest.RestsDefinition;
055import org.apache.camel.spi.ModelJAXBContextFactory;
056import org.apache.camel.spi.NamespaceAware;
057import org.apache.camel.spi.TypeConverterRegistry;
058import org.apache.camel.util.ObjectHelper;
059import org.apache.camel.util.xml.XmlLineNumberParser;
060
061import static org.apache.camel.model.ProcessorDefinitionHelper.filterTypeInOutputs;
062
063/**
064 * Helper for the Camel {@link org.apache.camel.model model} classes.
065 */
066public final class ModelHelper {
067
068    private ModelHelper() {
069        // utility class
070    }
071
072    /**
073     * Dumps the definition as XML
074     *
075     * @param context the CamelContext, if <tt>null</tt> then
076     *            {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in
077     *            use
078     * @param definition the definition, such as a
079     *            {@link org.apache.camel.NamedNode}
080     * @return the output in XML (is formatted)
081     * @throws JAXBException is throw if error marshalling to XML
082     */
083    public static String dumpModelAsXml(CamelContext context, NamedNode definition) throws JAXBException {
084        JAXBContext jaxbContext = getJAXBContext(context);
085        final Map<String, String> namespaces = new LinkedHashMap<>();
086
087        // gather all namespaces from the routes or route which is stored on the
088        // expression nodes
089        if (definition instanceof RoutesDefinition) {
090            List<RouteDefinition> routes = ((RoutesDefinition)definition).getRoutes();
091            for (RouteDefinition route : routes) {
092                extractNamespaces(route, namespaces);
093            }
094        } else if (definition instanceof RouteDefinition) {
095            RouteDefinition route = (RouteDefinition)definition;
096            extractNamespaces(route, namespaces);
097        }
098
099        Marshaller marshaller = jaxbContext.createMarshaller();
100        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
101        marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
102        StringWriter buffer = new StringWriter();
103        marshaller.marshal(definition, buffer);
104
105        XmlConverter xmlConverter = newXmlConverter(context);
106        String xml = buffer.toString();
107        Document dom;
108        try {
109            dom = xmlConverter.toDOMDocument(xml, null);
110        } catch (Exception e) {
111            throw new TypeConversionException(xml, Document.class, e);
112        }
113
114        // Add additional namespaces to the document root element
115        Element documentElement = dom.getDocumentElement();
116        for (String nsPrefix : namespaces.keySet()) {
117            String prefix = nsPrefix.equals("xmlns") ? nsPrefix : "xmlns:" + nsPrefix;
118            documentElement.setAttribute(prefix, namespaces.get(nsPrefix));
119        }
120
121        // We invoke the type converter directly because we need to pass some
122        // custom XML output options
123        Properties outputProperties = new Properties();
124        outputProperties.put(OutputKeys.INDENT, "yes");
125        outputProperties.put(OutputKeys.STANDALONE, "yes");
126        outputProperties.put(OutputKeys.ENCODING, "UTF-8");
127        try {
128            return xmlConverter.toStringFromDocument(dom, outputProperties);
129        } catch (TransformerException e) {
130            throw new IllegalStateException("Failed converting document object to string", e);
131        }
132    }
133
134    /**
135     * Dumps the definition as XML
136     *
137     * @param context the CamelContext, if <tt>null</tt> then
138     *            {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in
139     *            use
140     * @param definition the definition, such as a
141     *            {@link org.apache.camel.NamedNode}
142     * @param resolvePlaceholders whether to resolve property placeholders in
143     *            the dumped XML
144     * @param resolveDelegateEndpoints whether to resolve delegate endpoints in
145     *            the dumped XML (limited to endpoints used in uri attributes in
146     *            the model)
147     * @return the output in XML (is formatted)
148     * @throws Exception is throw if error marshalling to XML
149     */
150    public static String dumpModelAsXml(CamelContext context, NamedNode definition, boolean resolvePlaceholders, boolean resolveDelegateEndpoints) throws Exception {
151        String xml = ModelHelper.dumpModelAsXml(context, definition);
152
153        // if resolving placeholders we parse the xml, and resolve the property
154        // placeholders during parsing
155        if (resolvePlaceholders || resolveDelegateEndpoints) {
156            final AtomicBoolean changed = new AtomicBoolean();
157            InputStream is = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8));
158            Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() {
159
160                private String prev;
161
162                @Override
163                public String transform(String text) {
164                    String after = text;
165                    if (resolveDelegateEndpoints && "uri".equals(prev)) {
166                        try {
167                            // must resolve placeholder as the endpoint may use
168                            // property placeholders
169                            String uri = context.resolvePropertyPlaceholders(text);
170                            Endpoint endpoint = context.hasEndpoint(uri);
171                            if (endpoint instanceof DelegateEndpoint) {
172                                endpoint = ((DelegateEndpoint)endpoint).getEndpoint();
173                                after = endpoint.getEndpointUri();
174                            }
175                        } catch (Exception e) {
176                            // ignore
177                        }
178                    }
179
180                    if (resolvePlaceholders) {
181                        try {
182                            after = context.resolvePropertyPlaceholders(after);
183                        } catch (Exception e) {
184                            // ignore
185                        }
186                    }
187
188                    if (!changed.get()) {
189                        changed.set(!text.equals(after));
190                    }
191
192                    // okay the previous must be the attribute key with uri, so
193                    // it refers to an endpoint
194                    prev = text;
195
196                    return after;
197                }
198            });
199
200            // okay there were some property placeholder or delegate endpoints
201            // replaced so re-create the model
202            if (changed.get()) {
203                xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom);
204                NamedNode copy = ModelHelper.createModelFromXml(context, xml, NamedNode.class);
205                xml = ModelHelper.dumpModelAsXml(context, copy);
206            }
207        }
208
209        return xml;
210    }
211
212    /**
213     * Marshal the xml to the model definition
214     *
215     * @param context the CamelContext, if <tt>null</tt> then
216     *            {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in
217     *            use
218     * @param xml the xml
219     * @param type the definition type to return, will throw a
220     *            {@link ClassCastException} if not the expected type
221     * @return the model definition
222     * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling
223     *             from xml to model
224     */
225    public static <T extends NamedNode> T createModelFromXml(CamelContext context, String xml, Class<T> type) throws JAXBException {
226        return modelToXml(context, null, xml, type);
227    }
228
229    /**
230     * Marshal the xml to the model definition
231     *
232     * @param context the CamelContext, if <tt>null</tt> then
233     *            {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in
234     *            use
235     * @param stream the xml stream
236     * @param type the definition type to return, will throw a
237     *            {@link ClassCastException} if not the expected type
238     * @return the model definition
239     * @throws javax.xml.bind.JAXBException is thrown if error unmarshalling
240     *             from xml to model
241     */
242    public static <T extends NamedNode> T createModelFromXml(CamelContext context, InputStream stream, Class<T> type) throws JAXBException {
243        return modelToXml(context, stream, null, type);
244    }
245
246    /**
247     * Marshal the xml to the model definition
248     *
249     * @param context the CamelContext, if <tt>null</tt> then
250     *            {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in
251     *            use
252     * @param inputStream the xml stream
253     * @throws Exception is thrown if an error is encountered unmarshalling from
254     *             xml to model
255     */
256    public static RoutesDefinition loadRoutesDefinition(CamelContext context, InputStream inputStream) throws Exception {
257        XmlConverter xmlConverter = newXmlConverter(context);
258        Document dom = xmlConverter.toDOMDocument(inputStream, null);
259        return loadRoutesDefinition(context, dom);
260    }
261
262    /**
263     * Marshal the xml to the model definition
264     *
265     * @param context the CamelContext, if <tt>null</tt> then
266     *            {@link org.apache.camel.spi.ModelJAXBContextFactory} is not in
267     *            use
268     * @param node the xml node
269     * @throws Exception is thrown if an error is encountered unmarshalling from
270     *             xml to model
271     */
272    public static RoutesDefinition loadRoutesDefinition(CamelContext context, Node node) throws Exception {
273        JAXBContext jaxbContext = getJAXBContext(context);
274
275        Map<String, String> namespaces = new LinkedHashMap<>();
276
277        Document dom = node instanceof Document ? (Document)node : node.getOwnerDocument();
278        extractNamespaces(dom, namespaces);
279
280        Binder<Node> binder = jaxbContext.createBinder();
281        Object result = binder.unmarshal(node);
282
283        if (result == null) {
284            throw new JAXBException("Cannot unmarshal to RoutesDefinition using JAXB");
285        }
286
287        // can either be routes or a single route
288        RoutesDefinition answer;
289        if (result instanceof RouteDefinition) {
290            RouteDefinition route = (RouteDefinition)result;
291            answer = new RoutesDefinition();
292            applyNamespaces(route, namespaces);
293            answer.getRoutes().add(route);
294        } else if (result instanceof RoutesDefinition) {
295            answer = (RoutesDefinition)result;
296            for (RouteDefinition route : answer.getRoutes()) {
297                applyNamespaces(route, namespaces);
298            }
299        } else {
300            throw new IllegalArgumentException("Unmarshalled object is an unsupported type: " + ObjectHelper.className(result) + " -> " + result);
301        }
302
303        return answer;
304    }
305
306    public static RestsDefinition loadRestsDefinition(CamelContext context, InputStream is) throws Exception {
307        // load routes using JAXB
308        Unmarshaller unmarshaller = getJAXBContext(context).createUnmarshaller();
309        Object result = unmarshaller.unmarshal(is);
310
311        if (result == null) {
312            throw new IOException("Cannot unmarshal to rests using JAXB from input stream: " + is);
313        }
314
315        // can either be routes or a single route
316        RestsDefinition answer;
317        if (result instanceof RestDefinition) {
318            RestDefinition rest = (RestDefinition)result;
319            answer = new RestsDefinition();
320            answer.getRests().add(rest);
321        } else if (result instanceof RestsDefinition) {
322            answer = (RestsDefinition)result;
323        } else {
324            throw new IllegalArgumentException("Unmarshalled object is an unsupported type: " + ObjectHelper.className(result) + " -> " + result);
325        }
326
327        return answer;
328    }
329
330    private static <T extends NamedNode> T modelToXml(CamelContext context, InputStream is, String xml, Class<T> type) throws JAXBException {
331        JAXBContext jaxbContext = getJAXBContext(context);
332
333        XmlConverter xmlConverter = newXmlConverter(context);
334        Document dom = null;
335        try {
336            if (is != null) {
337                dom = xmlConverter.toDOMDocument(is, null);
338            } else if (xml != null) {
339                dom = xmlConverter.toDOMDocument(xml, null);
340            }
341        } catch (Exception e) {
342            throw new TypeConversionException(xml, Document.class, e);
343        }
344        if (dom == null) {
345            throw new IllegalArgumentException("InputStream and XML is both null");
346        }
347
348        Map<String, String> namespaces = new LinkedHashMap<>();
349        extractNamespaces(dom, namespaces);
350
351        Binder<Node> binder = jaxbContext.createBinder();
352        Object result = binder.unmarshal(dom);
353
354        if (result == null) {
355            throw new JAXBException("Cannot unmarshal to " + type + " using JAXB");
356        }
357
358        // Restore namespaces to anything that's NamespaceAware
359        if (result instanceof RoutesDefinition) {
360            List<RouteDefinition> routes = ((RoutesDefinition)result).getRoutes();
361            for (RouteDefinition route : routes) {
362                applyNamespaces(route, namespaces);
363            }
364        } else if (result instanceof RouteDefinition) {
365            RouteDefinition route = (RouteDefinition)result;
366            applyNamespaces(route, namespaces);
367        }
368
369        return type.cast(result);
370    }
371
372    private static JAXBContext getJAXBContext(CamelContext context) throws JAXBException {
373        ModelJAXBContextFactory factory = context.adapt(ExtendedCamelContext.class).getModelJAXBContextFactory();
374        return factory.newJAXBContext();
375    }
376
377    private static void applyNamespaces(RouteDefinition route, Map<String, String> namespaces) {
378        Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class);
379        while (it.hasNext()) {
380            NamespaceAware na = getNamespaceAwareFromExpression(it.next());
381            if (na != null) {
382                na.setNamespaces(namespaces);
383            }
384        }
385    }
386
387    private static NamespaceAware getNamespaceAwareFromExpression(ExpressionNode expressionNode) {
388        ExpressionDefinition ed = expressionNode.getExpression();
389
390        NamespaceAware na = null;
391        Expression exp = ed.getExpressionValue();
392        if (exp instanceof NamespaceAware) {
393            na = (NamespaceAware)exp;
394        } else if (ed instanceof NamespaceAware) {
395            na = (NamespaceAware)ed;
396        }
397
398        return na;
399    }
400
401    /**
402     * Extract all XML namespaces from the expressions in the route
403     *
404     * @param route the route
405     * @param namespaces the map of namespaces to add discovered XML namespaces
406     *            into
407     */
408    private static void extractNamespaces(RouteDefinition route, Map<String, String> namespaces) {
409        Iterator<ExpressionNode> it = filterTypeInOutputs(route.getOutputs(), ExpressionNode.class);
410        while (it.hasNext()) {
411            NamespaceAware na = getNamespaceAwareFromExpression(it.next());
412
413            if (na != null) {
414                Map<String, String> map = na.getNamespaces();
415                if (map != null && !map.isEmpty()) {
416                    namespaces.putAll(map);
417                }
418            }
419        }
420    }
421
422    /**
423     * Extract all XML namespaces from the root element in a DOM Document
424     *
425     * @param document the DOM document
426     * @param namespaces the map of namespaces to add new found XML namespaces
427     */
428    private static void extractNamespaces(Document document, Map<String, String> namespaces) throws JAXBException {
429        NamedNodeMap attributes = document.getDocumentElement().getAttributes();
430        for (int i = 0; i < attributes.getLength(); i++) {
431            Node item = attributes.item(i);
432            String nsPrefix = item.getNodeName();
433            if (nsPrefix != null && nsPrefix.startsWith("xmlns")) {
434                String nsValue = item.getNodeValue();
435                String[] nsParts = nsPrefix.split(":");
436                if (nsParts.length == 1) {
437                    namespaces.put(nsParts[0], nsValue);
438                } else if (nsParts.length == 2) {
439                    namespaces.put(nsParts[1], nsValue);
440                } else {
441                    // Fallback on adding the namespace prefix as we find it
442                    namespaces.put(nsPrefix, nsValue);
443                }
444            }
445        }
446    }
447
448    /**
449     * Creates a new {@link XmlConverter}
450     *
451     * @param context CamelContext if provided
452     * @return a new XmlConverter instance
453     */
454    private static XmlConverter newXmlConverter(CamelContext context) {
455        XmlConverter xmlConverter;
456        if (context != null) {
457            TypeConverterRegistry registry = context.getTypeConverterRegistry();
458            xmlConverter = registry.getInjector().newInstance(XmlConverter.class, false);
459        } else {
460            xmlConverter = new XmlConverter();
461        }
462        return xmlConverter;
463    }
464
465}