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