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