content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.camel.component.rest;
  
  import java.io.InputStream;
  import java.util.Locale;
  
  import org.apache.camel.AsyncCallback;
  import org.apache.camel.AsyncProcessor;
  import org.apache.camel.CamelContext;
  import org.apache.camel.CamelContextAware;
  import org.apache.camel.CamelExchangeException;
  import org.apache.camel.Exchange;
  import org.apache.camel.spi.DataFormat;
  import org.apache.camel.support.ExchangeHelper;
  import org.apache.camel.support.processor.DelegateAsyncProcessor;
  import org.apache.camel.support.processor.MarshalProcessor;
  import org.apache.camel.support.processor.UnmarshalProcessor;
  import org.apache.camel.support.service.ServiceHelper;
  import org.apache.camel.util.ObjectHelper;
  
  /**
   * A {@link org.apache.camel.Processor} that binds the REST producer request and reply messages from sources of json or
   * xml to Java Objects.
   * <p/>
   * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform from xml/json to Java
   * Objects and reverse again.
   * <p/>
   * The rest-dsl consumer side is implemented in {@link org.apache.camel.processor.RestBindingAdvice}
   */
  public class RestProducerBindingProcessor extends DelegateAsyncProcessor {
  
      private final CamelContext camelContext;
      private final AsyncProcessor jsonUnmarshal;
      private final AsyncProcessor xmlUnmarshal;
      private final AsyncProcessor jsonMarshal;
      private final AsyncProcessor xmlMarshal;
      private final String bindingMode;
      private final boolean skipBindingOnErrorCode;
      private final String outType;
  
      public RestProducerBindingProcessor(AsyncProcessor processor, CamelContext camelContext,
                                          DataFormat jsonDataFormat, DataFormat xmlDataFormat,
                                          DataFormat outJsonDataFormat, DataFormat outXmlDataFormat,
                                          String bindingMode, boolean skipBindingOnErrorCode,
                                          String outType) {
  
          super(processor);
  
          this.camelContext = camelContext;
  
          if (outJsonDataFormat != null) {
              this.jsonUnmarshal = new UnmarshalProcessor(outJsonDataFormat);
          } else {
              this.jsonUnmarshal = null;
          }
          if (jsonDataFormat != null) {
              this.jsonMarshal = new MarshalProcessor(jsonDataFormat);
          } else {
              this.jsonMarshal = null;
          }
  
          if (outXmlDataFormat != null) {
              this.xmlUnmarshal = new UnmarshalProcessor(outXmlDataFormat);
          } else {
              this.xmlUnmarshal = null;
          }
          if (xmlDataFormat != null) {
              this.xmlMarshal = new MarshalProcessor(xmlDataFormat);
          } else {
              this.xmlMarshal = null;
          }
  
          this.bindingMode = bindingMode;
          this.skipBindingOnErrorCode = skipBindingOnErrorCode;
          this.outType = outType;
      }
  
      @Override
      public String toString() {
          return "RestProducerBindingProcessor";
      }
  
      @Override
      protected void doStart() throws Exception {
          // inject CamelContext before starting
          if (jsonMarshal instanceof CamelContextAware camelContextAware) {
              camelContextAware.setCamelContext(camelContext);
          }
          if (jsonUnmarshal instanceof CamelContextAware camelContextAware) {
              camelContextAware.setCamelContext(camelContext);
          }
          if (xmlMarshal instanceof CamelContextAware camelContextAware) {
              camelContextAware.setCamelContext(camelContext);
          }
          if (xmlUnmarshal instanceof CamelContextAware camelContextAware) {
              camelContextAware.setCamelContext(camelContext);
          }
          ServiceHelper.startService(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
      }
  
      @Override
      protected void doStop() throws Exception {
          ServiceHelper.stopService(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal);
      }
  
      @Override
      public boolean process(Exchange exchange, AsyncCallback callback) {
          boolean isXml = false;
          boolean isJson = false;
  
          // skip before binding for empty/null body
          Object body = exchange.getIn().getBody();
          if (ObjectHelper.isEmpty(body)) {
              if (outType != null) {
                  // wrap callback to add reverse operation if we know the output type from the REST service
                  callback = new RestProducerBindingUnmarshalCallback(exchange, callback, false);
              }
              // okay now we can continue routing to the producer
              return getProcessor().process(exchange, callback);
          }
  
          // we only need to perform before binding if the message body is POJO based
          if (body instanceof String || body instanceof byte[]) {
              // the body is text based and thus not POJO so no binding needed
              if (outType != null) {
                  // wrap callback to add reverse operation if we know the output type from the REST service
                  callback = new RestProducerBindingUnmarshalCallback(exchange, callback, false);
              }
              // okay now we can continue routing to the producer
              return getProcessor().process(exchange, callback);
          } else {
              // if its convertable to stream based then its not POJO based
              InputStream is = camelContext.getTypeConverter().tryConvertTo(InputStream.class, exchange, body);
              if (is != null) {
                  exchange.getIn().setBody(is);
                  if (outType != null) {
                      // wrap callback to add reverse operation if we know the output type from the REST service
                      callback = new RestProducerBindingUnmarshalCallback(exchange, callback, false);
                  }
                  // okay now we can continue routing to the producer
                  return getProcessor().process(exchange, callback);
              }
          }
  
          // assume body is POJO based and binding needed
  
          String contentType = ExchangeHelper.getContentType(exchange);
          if (contentType != null) {
              isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
              isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
          }
  
          // only allow xml/json if the binding mode allows that
          isXml &= bindingMode.equals("auto") || bindingMode.contains("xml");
          isJson &= bindingMode.equals("auto") || bindingMode.contains("json");
  
          // if we do not yet know if its xml or json, then use the binding mode to know the mode
          if (!isJson && !isXml) {
              isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
              isJson = bindingMode.equals("auto") || bindingMode.contains("json");
          }
  
          // favor json over xml
          if (isJson && jsonMarshal != null) {
              try {
                  jsonMarshal.process(exchange);
              } catch (Exception e) {
                  // we failed so cannot call producer
                  exchange.setException(e);
                  callback.done(true);
                  return true;
              }
              // need to prepare exchange first
              ExchangeHelper.prepareOutToIn(exchange);
              if (outType != null) {
                  // wrap callback to add reverse operation if we know the output type from the REST service
                  callback = new RestProducerBindingUnmarshalCallback(exchange, callback, false);
              }
              // okay now we can continue routing to the producer
              return getProcessor().process(exchange, callback);
          } else if (isXml && xmlMarshal != null) {
              try {
                  xmlMarshal.process(exchange);
              } catch (Exception e) {
                  // we failed so cannot call producer
                  exchange.setException(e);
                  callback.done(true);
                  return true;
              }
              // need to prepare exchange first
              ExchangeHelper.prepareOutToIn(exchange);
              if (outType != null) {
                  // wrap callback to add reverse operation if we know the output type from the REST service
                  callback = new RestProducerBindingUnmarshalCallback(exchange, callback, true);
              }
              // okay now we can continue routing to the producer
              return getProcessor().process(exchange, callback);
          }
  
          // we could not bind
          if ("off".equals(bindingMode) || bindingMode.equals("auto")) {
              if (outType != null) {
                  // wrap callback to add reverse operation if we know the output type from the REST service
                  callback = new RestProducerBindingUnmarshalCallback(exchange, callback, false);
              }
              // okay now we can continue routing to the producer
              return getProcessor().process(exchange, callback);
          } else {
              if (bindingMode.contains("xml")) {
                  exchange.setException(
                          new CamelExchangeException("Cannot bind to xml as message body is not xml compatible", exchange));
              } else {
                  exchange.setException(
                          new CamelExchangeException("Cannot bind to json as message body is not json compatible", exchange));
              }
              // we failed so cannot call producer
              callback.done(true);
              return true;
          }
      }
  
      private final class RestProducerBindingUnmarshalCallback implements AsyncCallback {
  
          private final Exchange exchange;
          private final AsyncCallback callback;
          private boolean wasXml;
  
          private RestProducerBindingUnmarshalCallback(Exchange exchange, AsyncCallback callback, boolean wasXml) {
              this.exchange = exchange;
              this.callback = callback;
              this.wasXml = wasXml;
          }
  
          @Override
          public void done(boolean doneSync) {
              try {
                  doDone();
              } catch (Exception e) {
                  exchange.setException(e);
              } finally {
                  // ensure callback is called
                  callback.done(doneSync);
              }
          }
  
          private void doDone() {
              // only unmarshal if there was no exception
              if (exchange.getException() != null) {
                  return;
              }
  
              if (skipBindingOnErrorCode) {
                  Integer code = exchange.getMessage().getHeader(RestConstants.HTTP_RESPONSE_CODE, Integer.class);
                  // if there is a custom http error code then skip binding
                  if (code != null && code >= 300) {
                      return;
                  }
              }
  
              boolean isXml = false;
              boolean isJson = false;
  
              // check the content-type if its json or xml
              String contentType = ExchangeHelper.getContentType(exchange);
              if (contentType != null) {
                  isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml");
                  isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json");
              }
  
              // only allow xml/json if the binding mode allows that (when off we still want to know if its xml or json)
              if (bindingMode != null) {
                  isXml &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("xml");
                  isJson &= bindingMode.equals("off") || bindingMode.equals("auto") || bindingMode.contains("json");
  
                  // if we do not yet know if its xml or json, then use the binding mode to know the mode
                  if (!isJson && !isXml) {
                      isXml = bindingMode.equals("auto") || bindingMode.contains("xml");
                      isJson = bindingMode.equals("auto") || bindingMode.contains("json");
                  }
              }
  
              // in case we have not yet been able to determine if xml or json, then use the same as in the unmarshaller
              if (isXml && isJson) {
                  isXml = wasXml;
                  isJson = !wasXml;
              }
  
              // need to prepare exchange first
              ExchangeHelper.prepareOutToIn(exchange);
  
              // ensure there is a content type header (even if binding is off)
              ensureHeaderContentType(isXml, isJson, exchange);
  
              if (bindingMode == null || "off".equals(bindingMode)) {
                  // binding is off, so no message body binding
                  return;
              }
  
              // is there any unmarshaller at all
              if (jsonUnmarshal == null && xmlUnmarshal == null) {
                  return;
              }
  
              // is the body empty
              if (exchange.hasOut() && exchange.getOut().getBody() == null
                      || !exchange.hasOut() && exchange.getIn().getBody() == null) {
                  return;
              }
  
              contentType = exchange.getIn().getHeader(RestConstants.CONTENT_TYPE, String.class);
              // need to lower-case so the contains check below can match if using upper case
              contentType = contentType.toLowerCase(Locale.US);
              try {
                  // favor json over xml
                  if (isJson && jsonUnmarshal != null) {
                      // only marshal if its json content type
                      if (contentType.contains("json")) {
                          jsonUnmarshal.process(exchange);
                      }
                  } else if (isXml && xmlUnmarshal != null) {
                      // only marshal if its xml content type
                      if (contentType.contains("xml")) {
                          xmlUnmarshal.process(exchange);
                      }
                  } else {
                      // we could not bind
                      if (bindingMode.equals("auto")) {
                          // okay for auto we do not mind if we could not bind
                      } else {
                          if (bindingMode.contains("xml")) {
                              exchange.setException(new CamelExchangeException(
                                      "Cannot bind from xml as message body is not xml compatible", exchange));
                          } else {
                              exchange.setException(new CamelExchangeException(
                                      "Cannot bind from json as message body is not json compatible", exchange));
                          }
                      }
                  }
              } catch (Exception e) {
                  exchange.setException(e);
              }
          }
  
          private void ensureHeaderContentType(boolean isXml, boolean isJson, Exchange exchange) {
              // favor json over xml
              if (isJson) {
                  // make sure there is a content-type with json
                  String type = ExchangeHelper.getContentType(exchange);
                  if (type == null) {
                      exchange.getIn().setHeader(RestConstants.CONTENT_TYPE, "application/json");
                  }
              } else if (isXml) {
                  // make sure there is a content-type with xml
                  String type = ExchangeHelper.getContentType(exchange);
                  if (type == null) {
                      exchange.getIn().setHeader(RestConstants.CONTENT_TYPE, "application/xml");
                  }
              }
          }
  
          @Override
          public String toString() {
              return "RestProducerBindingUnmarshalCallback";
          }
      }
  
  }
  