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.processor.binding;
018
019import java.util.Locale;
020import java.util.Map;
021
022import org.apache.camel.AsyncCallback;
023import org.apache.camel.AsyncProcessor;
024import org.apache.camel.CamelContext;
025import org.apache.camel.CamelContextAware;
026import org.apache.camel.Exchange;
027import org.apache.camel.Message;
028import org.apache.camel.Route;
029import org.apache.camel.processor.MarshalProcessor;
030import org.apache.camel.processor.UnmarshalProcessor;
031import org.apache.camel.spi.DataFormat;
032import org.apache.camel.spi.RestConfiguration;
033import org.apache.camel.support.ServiceSupport;
034import org.apache.camel.support.SynchronizationAdapter;
035import org.apache.camel.util.AsyncProcessorHelper;
036import org.apache.camel.util.ExchangeHelper;
037import org.apache.camel.util.MessageHelper;
038import org.apache.camel.util.ObjectHelper;
039import org.apache.camel.util.ServiceHelper;
040
041/**
042 * A {@link org.apache.camel.Processor} that binds the REST DSL incoming and outgoing messages
043 * from sources of json or xml to Java Objects.
044 * <p/>
045 * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform
046 * from xml/json to Java Objects and reverse again.
047 */
048public class RestBindingProcessor extends ServiceSupport implements AsyncProcessor {
049
050    private final CamelContext camelContext;
051    private final AsyncProcessor jsonUnmarshal;
052    private final AsyncProcessor xmlUnmarshal;
053    private final AsyncProcessor jsonMarshal;
054    private final AsyncProcessor xmlMarshal;
055    private final String consumes;
056    private final String produces;
057    private final String bindingMode;
058    private final boolean skipBindingOnErrorCode;
059    private final boolean enableCORS;
060    private final Map<String, String> corsHeaders;
061
062    public RestBindingProcessor(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat,
063                                DataFormat outJsonDataFormat, DataFormat outXmlDataFormat,
064                                String consumes, String produces, String bindingMode,
065                                boolean skipBindingOnErrorCode, boolean enableCORS,
066                                Map<String, String> corsHeaders) {
067
068        this.camelContext = camelContext;
069
070        if (jsonDataFormat != null) {
071            this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat);
072        } else {
073            this.jsonUnmarshal = null;
074        }
075        if (outJsonDataFormat != null) {
076            this.jsonMarshal = new MarshalProcessor(outJsonDataFormat);
077        } else if (jsonDataFormat != null) {
078            this.jsonMarshal = new MarshalProcessor(jsonDataFormat);
079        } else {
080            this.jsonMarshal = null;
081        }
082
083        if (xmlDataFormat != null) {
084            this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat);
085        } else {
086            this.xmlUnmarshal = null;
087        }
088        if (outXmlDataFormat != null) {
089            this.xmlMarshal = new MarshalProcessor(outXmlDataFormat);
090        } else if (xmlDataFormat != null) {
091            this.xmlMarshal = new MarshalProcessor(xmlDataFormat);
092        } else {
093            this.xmlMarshal = null;
094        }
095
096        this.consumes = consumes;
097        this.produces = produces;
098        this.bindingMode = bindingMode;
099        this.skipBindingOnErrorCode = skipBindingOnErrorCode;
100        this.enableCORS = enableCORS;
101        this.corsHeaders = corsHeaders;
102    }
103
104    @Override
105    public void process(Exchange exchange) throws Exception {
106        AsyncProcessorHelper.process(this, exchange);
107    }
108
109    @Override
110    public boolean process(Exchange exchange, final AsyncCallback callback) {
111        if (enableCORS) {
112            exchange.addOnCompletion(new RestBindingCORSOnCompletion(corsHeaders));
113        }
114
115        boolean isXml = false;
116        boolean isJson = false;
117
118        String contentType = ExchangeHelper.getContentType(exchange);
119        if (contentType != null) {
120            isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
121            isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
122        }
123        // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with
124        // that information in the consumes
125        if (!isXml && !isJson) {
126            isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml");
127            isJson = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("json");
128        }
129
130        // only allow xml/json if the binding mode allows that
131        isXml &= bindingMode.equals("auto") || bindingMode.contains("xml");
132        isJson &= bindingMode.equals("auto") || bindingMode.contains("json");
133
134        // if we do not yet know if its xml or json, then use the binding mode to know the mode
135        if (!isJson && !isXml) {
136            isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
137            isJson = bindingMode.equals("auto") || bindingMode.contains("json");
138        }
139
140        String accept = exchange.getIn().getHeader("Accept", String.class);
141
142        String body = null;
143        if (exchange.getIn().getBody() != null) {
144
145           // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail
146            // as they assume a non-empty body
147            if (isXml || isJson) {
148                // we have binding enabled, so we need to know if there body is empty or not\
149                // so force reading the body as a String which we can work with
150                body = MessageHelper.extractBodyAsString(exchange.getIn());
151                if (body != null) {
152                    exchange.getIn().setBody(body);
153
154                    if (isXml && isJson) {
155                        // we have still not determined between xml or json, so check the body if its xml based or not
156                        isXml = body.startsWith("<");
157                        isJson = !isXml;
158                    }
159                }
160            }
161        }
162
163        // favor json over xml
164        if (isJson && jsonUnmarshal != null) {
165            // add reverse operation
166            exchange.addOnCompletion(new RestBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept));
167            if (ObjectHelper.isNotEmpty(body)) {
168                return jsonUnmarshal.process(exchange, callback);
169            } else {
170                callback.done(true);
171                return true;
172            }
173        } else if (isXml && xmlUnmarshal != null) {
174            // add reverse operation
175            exchange.addOnCompletion(new RestBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, true, accept));
176            if (ObjectHelper.isNotEmpty(body)) {
177                return xmlUnmarshal.process(exchange, callback);
178            } else {
179                callback.done(true);
180                return true;
181            }
182        }
183
184        // we could not bind
185        if (bindingMode == null || "off".equals(bindingMode) || bindingMode.equals("auto")) {
186            // okay for auto we do not mind if we could not bind
187            exchange.addOnCompletion(new RestBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept));
188            callback.done(true);
189            return true;
190        } else {
191            if (bindingMode.contains("xml")) {
192                exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange));
193            } else {
194                exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange));
195            }
196            callback.done(true);
197            return true;
198        }
199    }
200
201    @Override
202    public String toString() {
203        return "RestBindingProcessor";
204    }
205
206    @Override
207    protected void doStart() throws Exception {
208        // inject CamelContext before starting
209        if (jsonMarshal instanceof CamelContextAware) {
210            ((CamelContextAware) jsonMarshal).setCamelContext(camelContext);
211        }
212        if (jsonUnmarshal instanceof CamelContextAware) {
213            ((CamelContextAware) jsonUnmarshal).setCamelContext(camelContext);
214        }
215        if (xmlMarshal instanceof CamelContextAware) {
216            ((CamelContextAware) xmlMarshal).setCamelContext(camelContext);
217        }
218        if (xmlUnmarshal instanceof CamelContextAware) {
219            ((CamelContextAware) xmlUnmarshal).setCamelContext(camelContext);
220        }
221        ServiceHelper.startServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
222    }
223
224    @Override
225    protected void doStop() throws Exception {
226        ServiceHelper.stopServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
227    }
228
229    /**
230     * An {@link org.apache.camel.spi.Synchronization} that does the reverse operation
231     * of marshalling from POJO to json/xml
232     */
233    private final class RestBindingMarshalOnCompletion extends SynchronizationAdapter {
234
235        private final AsyncProcessor jsonMarshal;
236        private final AsyncProcessor xmlMarshal;
237        private final String routeId;
238        private boolean wasXml;
239        private String accept;
240
241        private RestBindingMarshalOnCompletion(String routeId, AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml, String accept) {
242            this.routeId = routeId;
243            this.jsonMarshal = jsonMarshal;
244            this.xmlMarshal = xmlMarshal;
245            this.wasXml = wasXml;
246            this.accept = accept;
247        }
248
249        @Override
250        public void onAfterRoute(Route route, Exchange exchange) {
251            // we use the onAfterRoute callback, to ensure the data has been marshalled before
252            // the consumer writes the response back
253
254            // only trigger when it was the 1st route that was done
255            if (!routeId.equals(route.getId())) {
256                return;
257            }
258
259            // only marshal if there was no exception
260            if (exchange.getException() != null) {
261                return;
262            }
263
264            if (skipBindingOnErrorCode) {
265                Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class);
266                // if there is a custom http error code then skip binding
267                if (code != null && code >= 300) {
268                    return;
269                }
270            }
271
272            boolean isXml = false;
273            boolean isJson = false;
274
275            // accept takes precedence
276            if (accept != null) {
277                isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml");
278                isJson = accept.toLowerCase(Locale.ENGLISH).contains("json");
279            }
280            // fallback to content type if still undecided
281            if (!isXml && !isJson) {
282                String contentType = ExchangeHelper.getContentType(exchange);
283                if (contentType != null) {
284                    isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
285                    isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
286                }
287            }
288            // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with
289            // that information in the consumes
290            if (!isXml && !isJson) {
291                isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml");
292                isJson = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("json");
293            }
294
295            // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json)
296            if (bindingMode != null) {
297                isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml");
298                isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json");
299
300                // if we do not yet know if its xml or json, then use the binding mode to know the mode
301                if (!isJson && !isXml) {
302                    isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
303                    isJson = bindingMode.equals("auto") || bindingMode.contains("json");
304                }
305            }
306
307            // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller
308            if (isXml && isJson) {
309                isXml = wasXml;
310                isJson = !wasXml;
311            }
312
313            // need to prepare exchange first
314            ExchangeHelper.prepareOutToIn(exchange);
315
316            // ensure there is a content type header (even if binding is off)
317            ensureHeaderContentType(produces, isXml, isJson, exchange);
318
319            if (bindingMode == null || "off".equals(bindingMode)) {
320                // binding is off, so no message body binding
321                return;
322            }
323
324            // is there any marshaller at all
325            if (jsonMarshal == null && xmlMarshal == null) {
326                return;
327            }
328
329            // is the body empty
330            if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) {
331                return;
332            }
333
334            String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class);
335            // need to lower-case so the contains check below can match if using upper case
336            contentType = contentType.toLowerCase(Locale.US);
337            try {
338                // favor json over xml
339                if (isJson && jsonMarshal != null) {
340                    // only marshal if its json content type
341                    if (contentType.contains("json")) {
342                        jsonMarshal.process(exchange);
343                    }
344                } else if (isXml && xmlMarshal != null) {
345                    // only marshal if its xml content type
346                    if (contentType.contains("xml")) {
347                        xmlMarshal.process(exchange);
348                    }
349                } else {
350                    // we could not bind
351                    if (bindingMode.equals("auto")) {
352                        // okay for auto we do not mind if we could not bind
353                    } else {
354                        if (bindingMode.contains("xml")) {
355                            exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange));
356                        } else {
357                            exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange));
358                        }
359                    }
360                }
361            } catch (Throwable e) {
362                exchange.setException(e);
363            }
364        }
365
366        private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) {
367            // favor given content type
368            if (contentType != null) {
369                String type = ExchangeHelper.getContentType(exchange);
370                if (type == null) {
371                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType);
372                }
373            }
374
375            // favor json over xml
376            if (isJson) {
377                // make sure there is a content-type with json
378                String type = ExchangeHelper.getContentType(exchange);
379                if (type == null) {
380                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json");
381                }
382            } else if (isXml) {
383                // make sure there is a content-type with xml
384                String type = ExchangeHelper.getContentType(exchange);
385                if (type == null) {
386                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml");
387                }
388            }
389        }
390
391        @Override
392        public String toString() {
393            return "RestBindingMarshalOnCompletion";
394        }
395    }
396
397    private final class RestBindingCORSOnCompletion extends SynchronizationAdapter {
398
399        private final Map<String, String> corsHeaders;
400
401        private RestBindingCORSOnCompletion(Map<String, String> corsHeaders) {
402            this.corsHeaders = corsHeaders;
403        }
404
405        @Override
406        public void onAfterRoute(Route route, Exchange exchange) {
407            // add the CORS headers after routing, but before the consumer writes the response
408            Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
409
410            // use default value if none has been configured
411            String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null;
412            if (allowOrigin == null) {
413                allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN;
414            }
415            String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null;
416            if (allowMethods == null) {
417                allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS;
418            }
419            String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null;
420            if (allowHeaders == null) {
421                allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS;
422            }
423            String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null;
424            if (maxAge == null) {
425                maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE;
426            }
427
428            msg.setHeader("Access-Control-Allow-Origin", allowOrigin);
429            msg.setHeader("Access-Control-Allow-Methods", allowMethods);
430            msg.setHeader("Access-Control-Allow-Headers", allowHeaders);
431            msg.setHeader("Access-Control-Max-Age", maxAge);
432        }
433
434        @Override
435        public String toString() {
436            return "RestBindingCORSOnCompletion";
437        }
438    }
439
440}