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.io.InputStream;
020import java.util.Locale;
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.processor.DelegateAsyncProcessor;
028import org.apache.camel.processor.MarshalProcessor;
029import org.apache.camel.processor.UnmarshalProcessor;
030import org.apache.camel.processor.binding.BindingException;
031import org.apache.camel.spi.DataFormat;
032import org.apache.camel.util.ExchangeHelper;
033import org.apache.camel.util.ObjectHelper;
034import org.apache.camel.util.ServiceHelper;
035
036/**
037 * A {@link org.apache.camel.Processor} that binds the REST producer request and reply messages
038 * from sources of json or xml to Java Objects.
039 * <p/>
040 * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform
041 * from xml/json to Java Objects and reverse again.
042 * <p/>
043 * The rest-dsl consumer side is implemented in {@link org.apache.camel.processor.RestBindingAdvice}
044 */
045public class RestProducerBindingProcessor extends DelegateAsyncProcessor {
046
047    private final CamelContext camelContext;
048    private final AsyncProcessor jsonUnmarshal;
049    private final AsyncProcessor xmlUnmarshal;
050    private final AsyncProcessor jsonMarshal;
051    private final AsyncProcessor xmlMarshal;
052    private final String bindingMode;
053    private final boolean skipBindingOnErrorCode;
054    private final String outType;
055
056    public RestProducerBindingProcessor(AsyncProcessor processor, CamelContext camelContext,
057                                        DataFormat jsonDataFormat, DataFormat xmlDataFormat,
058                                        DataFormat outJsonDataFormat, DataFormat outXmlDataFormat,
059                                        String bindingMode, boolean skipBindingOnErrorCode,
060                                        String outType) {
061
062        super(processor);
063
064        this.camelContext = camelContext;
065
066        if (outJsonDataFormat != null) {
067            this.jsonUnmarshal = new UnmarshalProcessor(outJsonDataFormat);
068        } else {
069            this.jsonUnmarshal = null;
070        }
071        if (jsonDataFormat != null) {
072            this.jsonMarshal = new MarshalProcessor(jsonDataFormat);
073        } else {
074            this.jsonMarshal = null;
075        }
076
077        if (outXmlDataFormat != null) {
078            this.xmlUnmarshal = new UnmarshalProcessor(outXmlDataFormat);
079        } else {
080            this.xmlUnmarshal = null;
081        }
082        if (xmlDataFormat != null) {
083            this.xmlMarshal = new MarshalProcessor(xmlDataFormat);
084        } else {
085            this.xmlMarshal = null;
086        }
087
088        this.bindingMode = bindingMode;
089        this.skipBindingOnErrorCode = skipBindingOnErrorCode;
090        this.outType = outType;
091    }
092
093    @Override
094    public String toString() {
095        return "RestProducerBindingProcessor";
096    }
097
098    @Override
099    protected void doStart() throws Exception {
100        // inject CamelContext before starting
101        if (jsonMarshal instanceof CamelContextAware) {
102            ((CamelContextAware) jsonMarshal).setCamelContext(camelContext);
103        }
104        if (jsonUnmarshal instanceof CamelContextAware) {
105            ((CamelContextAware) jsonUnmarshal).setCamelContext(camelContext);
106        }
107        if (xmlMarshal instanceof CamelContextAware) {
108            ((CamelContextAware) xmlMarshal).setCamelContext(camelContext);
109        }
110        if (xmlUnmarshal instanceof CamelContextAware) {
111            ((CamelContextAware) xmlUnmarshal).setCamelContext(camelContext);
112        }
113        ServiceHelper.startServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
114    }
115
116    @Override
117    protected void doStop() throws Exception {
118        ServiceHelper.stopServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
119    }
120
121    @Override
122    public boolean process(Exchange exchange, AsyncCallback callback) {
123        boolean isXml = false;
124        boolean isJson = false;
125
126        // skip before binding for empty/null body
127        Object body = exchange.getIn().getBody();
128        if (ObjectHelper.isEmpty(body)) {
129            if (outType != null) {
130                // wrap callback to add reverse operation if we know the output type from the REST service
131                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
132            }
133            // okay now we can continue routing to the producer
134            return getProcessor().process(exchange, callback);
135        }
136
137        // we only need to perform before binding if the message body is POJO based
138        if (body instanceof String || body instanceof byte[]) {
139            // the body is text based and thus not POJO so no binding needed
140            if (outType != null) {
141                // wrap callback to add reverse operation if we know the output type from the REST service
142                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
143            }
144            // okay now we can continue routing to the producer
145            return getProcessor().process(exchange, callback);
146        } else {
147            // if its convertable to stream based then its not POJO based
148            InputStream is = camelContext.getTypeConverter().tryConvertTo(InputStream.class, exchange, body);
149            if (is != null) {
150                exchange.getIn().setBody(is);
151                if (outType != null) {
152                    // wrap callback to add reverse operation if we know the output type from the REST service
153                    callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
154                }
155                // okay now we can continue routing to the producer
156                return getProcessor().process(exchange, callback);
157            }
158        }
159
160        // assume body is POJO based and binding needed
161
162        String contentType = ExchangeHelper.getContentType(exchange);
163        if (contentType != null) {
164            isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
165            isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
166        }
167
168        // only allow xml/json if the binding mode allows that
169        isXml &= bindingMode.equals("auto") || bindingMode.contains("xml");
170        isJson &= bindingMode.equals("auto") || bindingMode.contains("json");
171
172        // if we do not yet know if its xml or json, then use the binding mode to know the mode
173        if (!isJson && !isXml) {
174            isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
175            isJson = bindingMode.equals("auto") || bindingMode.contains("json");
176        }
177
178        // favor json over xml
179        if (isJson && jsonMarshal != null) {
180            try {
181                jsonMarshal.process(exchange);
182            } catch (Exception e) {
183                // we failed so cannot call producer
184                exchange.setException(e);
185                callback.done(true);
186                return true;
187            }
188            // need to prepare exchange first
189            ExchangeHelper.prepareOutToIn(exchange);
190            if (outType != null) {
191                // wrap callback to add reverse operation if we know the output type from the REST service
192                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
193            }
194            // okay now we can continue routing to the producer
195            return getProcessor().process(exchange, callback);
196        } else if (isXml && xmlMarshal != null) {
197            try {
198                xmlMarshal.process(exchange);
199            } catch (Exception e) {
200                // we failed so cannot call producer
201                exchange.setException(e);
202                callback.done(true);
203                return true;
204            }
205            // need to prepare exchange first
206            ExchangeHelper.prepareOutToIn(exchange);
207            if (outType != null) {
208                // wrap callback to add reverse operation if we know the output type from the REST service
209                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, true);
210            }
211            // okay now we can continue routing to the producer
212            return getProcessor().process(exchange, callback);
213        }
214
215        // we could not bind
216        if ("off".equals(bindingMode) || bindingMode.equals("auto")) {
217            if (outType != null) {
218                // wrap callback to add reverse operation if we know the output type from the REST service
219                callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false);
220            }
221            // okay now we can continue routing to the producer
222            return getProcessor().process(exchange, callback);
223        } else {
224            if (bindingMode.contains("xml")) {
225                exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange));
226            } else {
227                exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange));
228            }
229            // we failed so cannot call producer
230            callback.done(true);
231            return true;
232        }
233    }
234
235    private final class RestProducerBindingUnmarshalCallback implements AsyncCallback {
236
237        private final Exchange exchange;
238        private final AsyncCallback callback;
239        private final AsyncProcessor jsonMarshal;
240        private final AsyncProcessor xmlMarshal;
241        private boolean wasXml;
242
243        private RestProducerBindingUnmarshalCallback(Exchange exchange, AsyncCallback callback,
244                                                     AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml) {
245            this.exchange = exchange;
246            this.callback = callback;
247            this.jsonMarshal = jsonMarshal;
248            this.xmlMarshal = xmlMarshal;
249            this.wasXml = wasXml;
250        }
251
252        @Override
253        public void done(boolean doneSync) {
254            try {
255                doDone();
256            } catch (Throwable e) {
257                exchange.setException(e);
258            } finally {
259                // ensure callback is called
260                callback.done(doneSync);
261            }
262        }
263
264        private void doDone() {
265            // only unmarshal if there was no exception
266            if (exchange.getException() != null) {
267                return;
268            }
269
270            if (skipBindingOnErrorCode) {
271                Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class);
272                // if there is a custom http error code then skip binding
273                if (code != null && code >= 300) {
274                    return;
275                }
276            }
277
278            boolean isXml = false;
279            boolean isJson = false;
280
281            // check the content-type if its json or xml
282            String contentType = ExchangeHelper.getContentType(exchange);
283            if (contentType != null) {
284                isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
285                isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
286            }
287
288            // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json)
289            if (bindingMode != null) {
290                isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml");
291                isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json");
292
293                // if we do not yet know if its xml or json, then use the binding mode to know the mode
294                if (!isJson && !isXml) {
295                    isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
296                    isJson = bindingMode.equals("auto") || bindingMode.contains("json");
297                }
298            }
299
300            // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller
301            if (isXml && isJson) {
302                isXml = wasXml;
303                isJson = !wasXml;
304            }
305
306            // need to prepare exchange first
307            ExchangeHelper.prepareOutToIn(exchange);
308
309            // ensure there is a content type header (even if binding is off)
310            ensureHeaderContentType(isXml, isJson, exchange);
311
312            if (bindingMode == null || "off".equals(bindingMode)) {
313                // binding is off, so no message body binding
314                return;
315            }
316
317            // is there any unmarshaller at all
318            if (jsonUnmarshal == null && xmlUnmarshal == null) {
319                return;
320            }
321
322            // is the body empty
323            if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) {
324                return;
325            }
326
327            contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class);
328            // need to lower-case so the contains check below can match if using upper case
329            contentType = contentType.toLowerCase(Locale.US);
330            try {
331                // favor json over xml
332                if (isJson && jsonUnmarshal != null) {
333                    // only marshal if its json content type
334                    if (contentType.contains("json")) {
335                        jsonUnmarshal.process(exchange);
336                    }
337                } else if (isXml && xmlUnmarshal != null) {
338                    // only marshal if its xml content type
339                    if (contentType.contains("xml")) {
340                        xmlUnmarshal.process(exchange);
341                    }
342                } else {
343                    // we could not bind
344                    if (bindingMode.equals("auto")) {
345                        // okay for auto we do not mind if we could not bind
346                    } else {
347                        if (bindingMode.contains("xml")) {
348                            exchange.setException(new BindingException("Cannot bind from xml as message body is not xml compatible", exchange));
349                        } else {
350                            exchange.setException(new BindingException("Cannot bind from json as message body is not json compatible", exchange));
351                        }
352                    }
353                }
354            } catch (Throwable e) {
355                exchange.setException(e);
356            }
357        }
358
359        private void ensureHeaderContentType(boolean isXml, boolean isJson, Exchange exchange) {
360            // favor json over xml
361            if (isJson) {
362                // make sure there is a content-type with json
363                String type = ExchangeHelper.getContentType(exchange);
364                if (type == null) {
365                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json");
366                }
367            } else if (isXml) {
368                // make sure there is a content-type with xml
369                String type = ExchangeHelper.getContentType(exchange);
370                if (type == null) {
371                    exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/xml");
372                }
373            }
374        }
375
376        @Override
377        public String toString() {
378            return "RestProducerBindingUnmarshalCallback";
379        }
380    }
381
382}