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