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