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;
018
019import java.util.HashMap;
020import java.util.Locale;
021import java.util.Map;
022import java.util.Set;
023
024import org.apache.camel.AsyncProcessor;
025import org.apache.camel.CamelContext;
026import org.apache.camel.CamelContextAware;
027import org.apache.camel.Exchange;
028import org.apache.camel.Message;
029import org.apache.camel.processor.binding.BindingException;
030import org.apache.camel.spi.DataFormat;
031import org.apache.camel.spi.DataType;
032import org.apache.camel.spi.DataTypeAware;
033import org.apache.camel.spi.RestConfiguration;
034import org.apache.camel.util.ExchangeHelper;
035import org.apache.camel.util.MessageHelper;
036import org.apache.camel.util.ObjectHelper;
037
038/**
039 * A {@link org.apache.camel.processor.CamelInternalProcessorAdvice} that binds the REST DSL incoming
040 * and outgoing messages from sources of json or xml to Java Objects.
041 * <p/>
042 * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform
043 * from xml/json to Java Objects and reverse again.
044 * <p/>
045 * The rest producer side is implemented in {@link org.apache.camel.component.rest.RestProducerBindingProcessor}
046 *
047 * @see CamelInternalProcessor
048 */
049public class RestBindingAdvice implements CamelInternalProcessorAdvice<Map<String, Object>> {
050    private static final String STATE_KEY_DO_MARSHAL = "doMarshal";
051    private static final String STATE_KEY_ACCEPT = "accept";
052    private static final String STATE_JSON = "json";
053    private static final String STATE_XML = "xml";
054
055    private final AsyncProcessor jsonUnmarshal;
056    private final AsyncProcessor xmlUnmarshal;
057    private final AsyncProcessor jsonMarshal;
058    private final AsyncProcessor xmlMarshal;
059    private final String consumes;
060    private final String produces;
061    private final String bindingMode;
062    private final boolean skipBindingOnErrorCode;
063    private final boolean clientRequestValidation;
064    private final boolean enableCORS;
065    private final Map<String, String> corsHeaders;
066    private final Map<String, String> queryDefaultValues;
067    private final boolean requiredBody;
068    private final Set<String> requiredQueryParameters;
069    private final Set<String> requiredHeaders;
070
071    public RestBindingAdvice(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat,
072                             DataFormat outJsonDataFormat, DataFormat outXmlDataFormat,
073                             String consumes, String produces, String bindingMode,
074                             boolean skipBindingOnErrorCode, boolean clientRequestValidation, boolean enableCORS,
075                             Map<String, String> corsHeaders,
076                             Map<String, String> queryDefaultValues,
077                             boolean requiredBody, Set<String> requiredQueryParameters, Set<String> requiredHeaders) throws Exception {
078
079        if (jsonDataFormat != null) {
080            this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat);
081        } else {
082            this.jsonUnmarshal = null;
083        }
084        if (outJsonDataFormat != null) {
085            this.jsonMarshal = new MarshalProcessor(outJsonDataFormat);
086        } else if (jsonDataFormat != null) {
087            this.jsonMarshal = new MarshalProcessor(jsonDataFormat);
088        } else {
089            this.jsonMarshal = null;
090        }
091
092        if (xmlDataFormat != null) {
093            this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat);
094        } else {
095            this.xmlUnmarshal = null;
096        }
097        if (outXmlDataFormat != null) {
098            this.xmlMarshal = new MarshalProcessor(outXmlDataFormat);
099        } else if (xmlDataFormat != null) {
100            this.xmlMarshal = new MarshalProcessor(xmlDataFormat);
101        } else {
102            this.xmlMarshal = null;
103        }
104
105        if (jsonMarshal != null) {
106            camelContext.addService(jsonMarshal, true);
107        }
108        if (jsonUnmarshal != null) {
109            camelContext.addService(jsonUnmarshal, true);
110        }
111        if (xmlMarshal instanceof CamelContextAware) {
112            camelContext.addService(xmlMarshal, true);
113        }
114        if (xmlUnmarshal instanceof CamelContextAware) {
115            camelContext.addService(xmlUnmarshal, true);
116        }
117
118        this.consumes = consumes;
119        this.produces = produces;
120        this.bindingMode = bindingMode;
121        this.skipBindingOnErrorCode = skipBindingOnErrorCode;
122        this.clientRequestValidation = clientRequestValidation;
123        this.enableCORS = enableCORS;
124        this.corsHeaders = corsHeaders;
125        this.queryDefaultValues = queryDefaultValues;
126        this.requiredBody = requiredBody;
127        this.requiredQueryParameters = requiredQueryParameters;
128        this.requiredHeaders = requiredHeaders;
129    }
130    
131    @Override
132    public Map<String, Object> before(Exchange exchange) throws Exception {
133        Map<String, Object> state = new HashMap<>();
134        if (isOptionsMethod(exchange, state)) {
135            return state;
136        }
137        unmarshal(exchange, state);
138        return state;
139    }
140    
141    @Override
142    public void after(Exchange exchange, Map<String, Object> state) throws Exception {
143        if (enableCORS) {
144            setCORSHeaders(exchange, state);
145        }
146        if (state.get(STATE_KEY_DO_MARSHAL) != null) {
147            marshal(exchange, state);
148        }
149    }
150
151    private boolean isOptionsMethod(Exchange exchange, Map<String, Object> state) {
152        String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class);
153        if ("OPTIONS".equalsIgnoreCase(method)) {
154            // for OPTIONS methods then we should not route at all as its part of CORS
155            exchange.setProperty(Exchange.ROUTE_STOP, true);
156            return true;
157        }
158        return false;
159    }
160
161    private void unmarshal(Exchange exchange, Map<String, Object> state) throws Exception {
162        boolean isXml = false;
163        boolean isJson = false;
164
165        String contentType = ExchangeHelper.getContentType(exchange);
166        if (contentType != null) {
167            isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
168            isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
169        }
170        // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with
171        // that information in the consumes
172        if (!isXml && !isJson) {
173            isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml");
174            isJson = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("json");
175        }
176
177        // set data type if in use
178        if (exchange.getContext().isUseDataType()) {
179            if (exchange.getIn() instanceof DataTypeAware && (isJson || isXml)) {
180                ((DataTypeAware) exchange.getIn()).setDataType(new DataType(isJson ? "json" : "xml"));
181            }
182        }
183
184        // only allow xml/json if the binding mode allows that
185        isXml &= bindingMode.equals("auto") || bindingMode.contains("xml");
186        isJson &= bindingMode.equals("auto") || bindingMode.contains("json");
187
188        // if we do not yet know if its xml or json, then use the binding mode to know the mode
189        if (!isJson && !isXml) {
190            isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
191            isJson = bindingMode.equals("auto") || bindingMode.contains("json");
192        }
193
194        String accept = exchange.getMessage().getHeader("Accept", String.class);
195        state.put(STATE_KEY_ACCEPT, accept);
196
197        // perform client request validation
198        if (clientRequestValidation) {
199            // check if the content-type is accepted according to consumes
200            if (!isValidOrAcceptedContentType(consumes, contentType)) {
201                // the content-type is not something we can process so its a HTTP_ERROR 415
202                exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 415);
203                // stop routing and return
204                exchange.setProperty(Exchange.ROUTE_STOP, true);
205                return;
206            }
207
208            // check if what is produces is accepted by the client
209            if (!isValidOrAcceptedContentType(produces, accept)) {
210                // the response type is not accepted by the client so its a HTTP_ERROR 406
211                exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 406);
212                // stop routing and return
213                exchange.setProperty(Exchange.ROUTE_STOP, true);
214                return;
215            }
216        }
217
218        String body = null;
219        if (exchange.getIn().getBody() != null) {
220
221            // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail
222            // as they assume a non-empty body
223            if (isXml || isJson) {
224                // we have binding enabled, so we need to know if there body is empty or not
225                // so force reading the body as a String which we can work with
226                body = MessageHelper.extractBodyAsString(exchange.getIn());
227                if (body != null) {
228                    if (exchange.getIn() instanceof DataTypeAware) {
229                        ((DataTypeAware)exchange.getIn()).setBody(body, new DataType(isJson ? "json" : "xml"));
230                    } else {
231                        exchange.getIn().setBody(body);
232                    }
233
234                    if (isXml && isJson) {
235                        // we have still not determined between xml or json, so check the body if its xml based or not
236                        isXml = body.startsWith("<");
237                        isJson = !isXml;
238                    }
239                }
240            }
241        }
242
243        // add missing default values which are mapped as headers
244        if (queryDefaultValues != null) {
245            for (Map.Entry<String, String> entry : queryDefaultValues.entrySet()) {
246                if (exchange.getIn().getHeader(entry.getKey()) == null) {
247                    exchange.getIn().setHeader(entry.getKey(), entry.getValue());
248                }
249            }
250        }
251
252        // check for required
253        if (clientRequestValidation) {
254            if (requiredBody) {
255                // the body is required so we need to know if we have a body or not
256                // so force reading the body as a String which we can work with
257                if (body == null) {
258                    body = MessageHelper.extractBodyAsString(exchange.getIn());
259                    if (body != null) {
260                        exchange.getIn().setBody(body);
261                    }
262                }
263                if (body == null) {
264                    // this is a bad request, the client did not include a message body
265                    exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
266                    exchange.getOut().setBody("The request body is missing.");
267                    // stop routing and return
268                    exchange.setProperty(Exchange.ROUTE_STOP, true);
269                    return;
270                }
271            }
272            if (requiredQueryParameters != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredQueryParameters)) {
273                // this is a bad request, the client did not include some of the required query parameters
274                exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
275                exchange.getOut().setBody("Some of the required query parameters are missing.");
276                // stop routing and return
277                exchange.setProperty(Exchange.ROUTE_STOP, true);
278                return;
279            }
280            if (requiredHeaders != null && !exchange.getIn().getHeaders().keySet().containsAll(requiredHeaders)) {
281                // this is a bad request, the client did not include some of the required http headers
282                exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, 400);
283                exchange.getOut().setBody("Some of the required HTTP headers are missing.");
284                // stop routing and return
285                exchange.setProperty(Exchange.ROUTE_STOP, true);
286                return;
287            }
288        }
289
290        // favor json over xml
291        if (isJson && jsonUnmarshal != null) {
292            // add reverse operation
293            state.put(STATE_KEY_DO_MARSHAL, STATE_JSON);
294            if (ObjectHelper.isNotEmpty(body)) {
295                jsonUnmarshal.process(exchange);
296                ExchangeHelper.prepareOutToIn(exchange);
297            }
298            return;
299        } else if (isXml && xmlUnmarshal != null) {
300            // add reverse operation
301            state.put(STATE_KEY_DO_MARSHAL, STATE_XML);
302            if (ObjectHelper.isNotEmpty(body)) {
303                xmlUnmarshal.process(exchange);
304                ExchangeHelper.prepareOutToIn(exchange);
305            }
306            return;
307        }
308
309        // we could not bind
310        if ("off".equals(bindingMode) || bindingMode.equals("auto")) {
311            // okay for auto we do not mind if we could not bind
312            state.put(STATE_KEY_DO_MARSHAL, STATE_JSON);
313        } else {
314            if (bindingMode.contains("xml")) {
315                exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange));
316            } else {
317                exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange));
318            }
319        }
320        
321    }
322
323    private void marshal(Exchange exchange, Map<String, Object> state) {
324        // only marshal if there was no exception
325        if (exchange.getException() != null) {
326            return;
327        }
328
329        if (skipBindingOnErrorCode) {
330            Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class);
331            // if there is a custom http error code then skip binding
332            if (code != null && code >= 300) {
333                return;
334            }
335        }
336
337        boolean isXml = false;
338        boolean isJson = false;
339
340        // accept takes precedence
341        String accept = (String)state.get(STATE_KEY_ACCEPT);
342        if (accept != null) {
343            isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml");
344            isJson = accept.toLowerCase(Locale.ENGLISH).contains("json");
345        }
346        // fallback to content type if still undecided
347        if (!isXml && !isJson) {
348            String contentType = ExchangeHelper.getContentType(exchange);
349            if (contentType != null) {
350                isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
351                isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
352            }
353        }
354        // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with
355        // that information in the consumes
356        if (!isXml && !isJson) {
357            isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml");
358            isJson = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("json");
359        }
360
361        // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json)
362        if (bindingMode != null) {
363            isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml");
364            isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json");
365
366            // if we do not yet know if its xml or json, then use the binding mode to know the mode
367            if (!isJson && !isXml) {
368                isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
369                isJson = bindingMode.equals("auto") || bindingMode.contains("json");
370            }
371        }
372
373        // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller
374        if (isXml && isJson) {
375            isXml = state.get(STATE_KEY_DO_MARSHAL).equals(STATE_XML);
376            isJson = !isXml;
377        }
378
379        // need to prepare exchange first
380        ExchangeHelper.prepareOutToIn(exchange);
381
382        // ensure there is a content type header (even if binding is off)
383        ensureHeaderContentType(produces, isXml, isJson, exchange);
384
385        if (bindingMode == null || "off".equals(bindingMode)) {
386            // binding is off, so no message body binding
387            return;
388        }
389
390        // is there any marshaller at all
391        if (jsonMarshal == null && xmlMarshal == null) {
392            return;
393        }
394
395        // is the body empty
396        if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) {
397            return;
398        }
399
400        String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class);
401        // need to lower-case so the contains check below can match if using upper case
402        contentType = contentType.toLowerCase(Locale.US);
403        try {
404            // favor json over xml
405            if (isJson && jsonMarshal != null) {
406                // only marshal if its json content type
407                if (contentType.contains("json")) {
408                    jsonMarshal.process(exchange);
409                    setOutputDataType(exchange, new DataType("json"));
410                }
411            } else if (isXml && xmlMarshal != null) {
412                // only marshal if its xml content type
413                if (contentType.contains("xml")) {
414                    xmlMarshal.process(exchange);
415                    setOutputDataType(exchange, new DataType("xml"));
416                }
417            } else {
418                // we could not bind
419                if (bindingMode.equals("auto")) {
420                    // okay for auto we do not mind if we could not bind
421                } else {
422                    if (bindingMode.contains("xml")) {
423                        exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange));
424                    } else {
425                        exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange));
426                    }
427                }
428            }
429        } catch (Throwable e) {
430            exchange.setException(e);
431        }
432    }
433
434    private void setOutputDataType(Exchange exchange, DataType type) {
435        Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
436        if (target instanceof DataTypeAware) {
437            ((DataTypeAware)target).setDataType(type);
438        }
439    }
440
441    private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) {
442        // favor given content type
443        if (contentType != null) {
444            String type = ExchangeHelper.getContentType(exchange);
445            if (type == null) {
446                exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType);
447            }
448        }
449
450        // favor json over xml
451        if (isJson) {
452            // make sure there is a content-type with json
453            String type = ExchangeHelper.getContentType(exchange);
454            if (type == null) {
455                exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json");
456            }
457        } else if (isXml) {
458            // make sure there is a content-type with xml
459            String type = ExchangeHelper.getContentType(exchange);
460            if (type == null) {
461                exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml");
462            }
463        }
464    }
465
466    private void setCORSHeaders(Exchange exchange, Map<String, Object> state) {
467        // add the CORS headers after routing, but before the consumer writes the response
468        Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn();
469
470        // use default value if none has been configured
471        String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null;
472        if (allowOrigin == null) {
473            allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN;
474        }
475        String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null;
476        if (allowMethods == null) {
477            allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS;
478        }
479        String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null;
480        if (allowHeaders == null) {
481            allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS;
482        }
483        String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null;
484        if (maxAge == null) {
485            maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE;
486        }
487        String allowCredentials = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Credentials") : null;
488
489        // Restrict the origin if credentials are allowed.
490        // https://www.w3.org/TR/cors/ - section 6.1, point 3
491        String origin = exchange.getIn().getHeader("Origin", String.class);
492        if ("true".equalsIgnoreCase(allowCredentials) && "*".equals(allowOrigin) && origin != null) {
493            allowOrigin = origin;
494        }
495
496        msg.setHeader("Access-Control-Allow-Origin", allowOrigin);
497        msg.setHeader("Access-Control-Allow-Methods", allowMethods);
498        msg.setHeader("Access-Control-Allow-Headers", allowHeaders);
499        msg.setHeader("Access-Control-Max-Age", maxAge);
500        if (allowCredentials != null) {
501            msg.setHeader("Access-Control-Allow-Credentials", allowCredentials);
502        }
503    }
504
505    private static boolean isValidOrAcceptedContentType(String valid, String target) {
506        if (valid == null || target == null) {
507            return true;
508        }
509
510        boolean isXml = valid.toLowerCase(Locale.ENGLISH).contains("xml");
511        boolean isJson = valid.toLowerCase(Locale.ENGLISH).contains("json");
512
513        String type = target.toLowerCase(Locale.ENGLISH);
514
515        if (isXml && !type.contains("xml")) {
516            return false;
517        }
518        if (isJson && !type.contains("json")) {
519            return false;
520        }
521
522        return true;
523    }
524
525}