CAMEL-10164: Add support for binding in rest to
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/29cd3cdb Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/29cd3cdb Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/29cd3cdb Branch: refs/heads/master Commit: 29cd3cdbd23a8b7f94ac8818e8df5ce50f066d88 Parents: 737b49c Author: Claus Ibsen <davscl...@apache.org> Authored: Mon Sep 5 16:18:26 2016 +0200 Committer: Claus Ibsen <davscl...@apache.org> Committed: Mon Sep 5 16:26:13 2016 +0200 ---------------------------------------------------------------------- camel-core/src/main/docs/rest-component.adoc | 48 +- .../rest/RestConsumerBindingProcessor.java | 461 +++++++++++++++++++ .../camel/component/rest/RestEndpoint.java | 8 +- .../camel/component/rest/RestProducer.java | 76 ++- .../rest/RestProducerBindingProcessor.java | 124 ++--- .../camel/model/rest/RestBindingDefinition.java | 2 +- .../binding/RestConsumerBindingProcessor.java | 460 ------------------ .../JettyRestProducerPojoInOutTest.java | 8 +- .../src/test/resources/log4j2.properties | 2 +- 9 files changed, 658 insertions(+), 531 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/docs/rest-component.adoc ---------------------------------------------------------------------- diff --git a/camel-core/src/main/docs/rest-component.adoc b/camel-core/src/main/docs/rest-component.adoc index 2413940..910f271 100644 --- a/camel-core/src/main/docs/rest-component.adoc +++ b/camel-core/src/main/docs/rest-component.adoc @@ -54,9 +54,9 @@ The REST component supports 17 endpoint options which are listed below: | inType | common | | String | To declare the incoming POJO binding type as a FQN class name | outType | common | | String | To declare the outgoing POJO binding type as a FQN class name | produces | common | | String | Media type such as: 'text/xml' or 'application/json' this REST service returns. +| routeId | common | | String | Name of the route this REST services creates | bridgeErrorHandler | consumer | false | boolean | Allows for bridging the consumer to the Camel routing Error Handler which mean any exceptions occurred while the consumer is trying to pickup incoming messages or the likes will now be processed as a message and handled by the routing Error Handler. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions that will be logged at WARN/ERROR level and ignored. | description | consumer | | String | Human description to document this REST service -| routeId | consumer | | String | Name of the route this REST services creates | exceptionHandler | consumer (advanced) | | ExceptionHandler | To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this options is not in use. By default the consumer will deal with exceptions that will be logged at WARN/ERROR level and ignored. | apiDoc | producer | | String | The swagger api doc resource to use. The resource is loaded from classpath by default and must be in JSon format. | host | producer | | String | Host and port of HTTP service to use (override host in swagger schema) @@ -162,6 +162,52 @@ to use http4 you can do: -------------------------------------------- +[[Rest-Producer-Binding]] +Rest producer binding +^^^^^^^^^^^^^^^^^^^^^ + +The REST producer supports binding using JSon or XML like the rest-dsl does. + +For example to use jetty with json binding mode turned on you can configure this in the rest configuration: + +[source,java] +-------------------------------------------- + restConfiguration().component("jetty").host("localhost").port(8080).bindingMode(RestBindingMode.json); + + from("direct:start") + .to("rest:post:user"); +-------------------------------------------- + +Then when calling the REST service using rest producer it will automatic bind any POJOs to json before calling the REST service: + +[source,java] +-------------------------------------------- + UserPojo user = new UserPojo(); + user.setId(123); + user.setName("Donald Duck"); + + template.sendBody("direct:start", user); +-------------------------------------------- + +In the example above we send a POJO instance `UserPojo` as the message body. And because we have turned on JSon binding +in the rest configuration, then the POJO will be marshalled from POJO to JSon before calling the REST service. + +However if you want to also perform binding for the response message (eg what the REST service send back as response) you +would need to configure the `outType` option to specify what is the classname of the POJO to unmarshal from JSon to POJO. + +For example if the REST service returns a JSon payload that binds to `com.foo.MyResponsePojo` you can configure this as shown: + +[source,java] +-------------------------------------------- + restConfiguration().component("jetty").host("localhost").port(8080).bindingMode(RestBindingMode.json); + + from("direct:start") + .to("rest:post:user?outType=com.foo.MyResponsePojo"); +-------------------------------------------- + +IMPORTANT: You must configure `outType` option if you want POJO binding to happen for the response messages received from calling the REST service. + + [[Rest-Moreexamples]] More examples ^^^^^^^^^^^^^ http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/java/org/apache/camel/component/rest/RestConsumerBindingProcessor.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/rest/RestConsumerBindingProcessor.java b/camel-core/src/main/java/org/apache/camel/component/rest/RestConsumerBindingProcessor.java new file mode 100644 index 0000000..888fc2f --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/component/rest/RestConsumerBindingProcessor.java @@ -0,0 +1,461 @@ +/** + * 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.util.Locale; +import java.util.Map; + +import org.apache.camel.AsyncCallback; +import org.apache.camel.AsyncProcessor; +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Route; +import org.apache.camel.processor.MarshalProcessor; +import org.apache.camel.processor.UnmarshalProcessor; +import org.apache.camel.processor.binding.BindingException; +import org.apache.camel.spi.DataFormat; +import org.apache.camel.spi.RestConfiguration; +import org.apache.camel.support.ServiceSupport; +import org.apache.camel.support.SynchronizationAdapter; +import org.apache.camel.util.AsyncProcessorHelper; +import org.apache.camel.util.ExchangeHelper; +import org.apache.camel.util.MessageHelper; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.ServiceHelper; + +/** + * A {@link org.apache.camel.Processor} that binds the REST DSL incoming and outgoing 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. + */ +public class RestConsumerBindingProcessor extends ServiceSupport implements AsyncProcessor { + + private final CamelContext camelContext; + private final AsyncProcessor jsonUnmarshal; + private final AsyncProcessor xmlUnmarshal; + private final AsyncProcessor jsonMarshal; + private final AsyncProcessor xmlMarshal; + private final String consumes; + private final String produces; + private final String bindingMode; + private final boolean skipBindingOnErrorCode; + private final boolean enableCORS; + private final Map<String, String> corsHeaders; + private final Map<String, String> queryDefaultValues; + + public RestConsumerBindingProcessor(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, + DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, + String consumes, String produces, String bindingMode, + boolean skipBindingOnErrorCode, boolean enableCORS, + Map<String, String> corsHeaders, + Map<String, String> queryDefaultValues) { + + this.camelContext = camelContext; + + if (jsonDataFormat != null) { + this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat); + } else { + this.jsonUnmarshal = null; + } + if (outJsonDataFormat != null) { + this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); + } else if (jsonDataFormat != null) { + this.jsonMarshal = new MarshalProcessor(jsonDataFormat); + } else { + this.jsonMarshal = null; + } + + if (xmlDataFormat != null) { + this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat); + } else { + this.xmlUnmarshal = null; + } + if (outXmlDataFormat != null) { + this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); + } else if (xmlDataFormat != null) { + this.xmlMarshal = new MarshalProcessor(xmlDataFormat); + } else { + this.xmlMarshal = null; + } + + this.consumes = consumes; + this.produces = produces; + this.bindingMode = bindingMode; + this.skipBindingOnErrorCode = skipBindingOnErrorCode; + this.enableCORS = enableCORS; + this.corsHeaders = corsHeaders; + this.queryDefaultValues = queryDefaultValues; + } + + @Override + public void process(Exchange exchange) throws Exception { + AsyncProcessorHelper.process(this, exchange); + } + + @Override + public boolean process(Exchange exchange, final AsyncCallback callback) { + if (enableCORS) { + exchange.addOnCompletion(new RestConsumerBindingCORSOnCompletion(corsHeaders)); + } + + String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class); + if ("OPTIONS".equalsIgnoreCase(method)) { + // for OPTIONS methods then we should not route at all as its part of CORS + exchange.setProperty(Exchange.ROUTE_STOP, true); + callback.done(true); + return true; + } + + boolean isXml = false; + boolean isJson = false; + + String contentType = ExchangeHelper.getContentType(exchange); + if (contentType != null) { + isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); + isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); + } + // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with + // that information in the consumes + if (!isXml && !isJson) { + isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml"); + isJson = consumes != null && consumes.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"); + } + + String accept = exchange.getIn().getHeader("Accept", String.class); + + String body = null; + if (exchange.getIn().getBody() != null) { + + // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail + // as they assume a non-empty body + if (isXml || isJson) { + // we have binding enabled, so we need to know if there body is empty or not + // so force reading the body as a String which we can work with + body = MessageHelper.extractBodyAsString(exchange.getIn()); + if (body != null) { + exchange.getIn().setBody(body); + + if (isXml && isJson) { + // we have still not determined between xml or json, so check the body if its xml based or not + isXml = body.startsWith("<"); + isJson = !isXml; + } + } + } + } + + // add missing default values which are mapped as headers + if (queryDefaultValues != null) { + for (Map.Entry<String, String> entry : queryDefaultValues.entrySet()) { + if (exchange.getIn().getHeader(entry.getKey()) == null) { + exchange.getIn().setHeader(entry.getKey(), entry.getValue()); + } + } + } + + // favor json over xml + if (isJson && jsonUnmarshal != null) { + // add reverse operation + exchange.addOnCompletion(new RestConsumerBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept)); + if (ObjectHelper.isNotEmpty(body)) { + return jsonUnmarshal.process(exchange, callback); + } else { + callback.done(true); + return true; + } + } else if (isXml && xmlUnmarshal != null) { + // add reverse operation + exchange.addOnCompletion(new RestConsumerBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, true, accept)); + if (ObjectHelper.isNotEmpty(body)) { + return xmlUnmarshal.process(exchange, callback); + } else { + callback.done(true); + return true; + } + } + + // we could not bind + if ("off".equals(bindingMode) || bindingMode.equals("auto")) { + // okay for auto we do not mind if we could not bind + exchange.addOnCompletion(new RestConsumerBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept)); + callback.done(true); + return true; + } else { + if (bindingMode.contains("xml")) { + exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); + } else { + exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); + } + callback.done(true); + return true; + } + } + + @Override + public String toString() { + return "RestConsumerBindingProcessor"; + } + + @Override + protected void doStart() throws Exception { + // inject CamelContext before starting + if (jsonMarshal instanceof CamelContextAware) { + ((CamelContextAware) jsonMarshal).setCamelContext(camelContext); + } + if (jsonUnmarshal instanceof CamelContextAware) { + ((CamelContextAware) jsonUnmarshal).setCamelContext(camelContext); + } + if (xmlMarshal instanceof CamelContextAware) { + ((CamelContextAware) xmlMarshal).setCamelContext(camelContext); + } + if (xmlUnmarshal instanceof CamelContextAware) { + ((CamelContextAware) xmlUnmarshal).setCamelContext(camelContext); + } + ServiceHelper.startServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal); + } + + @Override + protected void doStop() throws Exception { + ServiceHelper.stopServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal); + } + + /** + * An {@link org.apache.camel.spi.Synchronization} that does the reverse operation + * of marshalling from POJO to json/xml + */ + private final class RestConsumerBindingMarshalOnCompletion extends SynchronizationAdapter { + + private final AsyncProcessor jsonMarshal; + private final AsyncProcessor xmlMarshal; + private final String routeId; + private boolean wasXml; + private String accept; + + private RestConsumerBindingMarshalOnCompletion(String routeId, AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml, String accept) { + this.routeId = routeId; + this.jsonMarshal = jsonMarshal; + this.xmlMarshal = xmlMarshal; + this.wasXml = wasXml; + this.accept = accept; + } + + @Override + public void onAfterRoute(Route route, Exchange exchange) { + // we use the onAfterRoute callback, to ensure the data has been marshalled before + // the consumer writes the response back + + // only trigger when it was the 1st route that was done + if (!routeId.equals(route.getId())) { + return; + } + + // only marshal if there was no exception + if (exchange.getException() != null) { + return; + } + + if (skipBindingOnErrorCode) { + Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.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; + + // accept takes precedence + if (accept != null) { + isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml"); + isJson = accept.toLowerCase(Locale.ENGLISH).contains("json"); + } + // fallback to content type if still undecided + if (!isXml && !isJson) { + String contentType = ExchangeHelper.getContentType(exchange); + if (contentType != null) { + isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); + isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); + } + } + // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with + // that information in the consumes + if (!isXml && !isJson) { + isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml"); + isJson = produces != null && produces.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(produces, isXml, isJson, exchange); + + if (bindingMode == null || "off".equals(bindingMode)) { + // binding is off, so no message body binding + return; + } + + // is there any marshaller at all + if (jsonMarshal == null && xmlMarshal == null) { + return; + } + + // is the body empty + if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) { + return; + } + + String contentType = exchange.getIn().getHeader(Exchange.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 && jsonMarshal != null) { + // only marshal if its json content type + if (contentType.contains("json")) { + jsonMarshal.process(exchange); + } + } else if (isXml && xmlMarshal != null) { + // only marshal if its xml content type + if (contentType.contains("xml")) { + xmlMarshal.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 BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); + } else { + exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); + } + } + } + } catch (Throwable e) { + exchange.setException(e); + } + } + + private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) { + // favor given content type + if (contentType != null) { + String type = ExchangeHelper.getContentType(exchange); + if (type == null) { + exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType); + } + } + + // 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(Exchange.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(Exchange.CONTENT_TYPE, "application/xml"); + } + } + } + + @Override + public String toString() { + return "RestConsumerBindingMarshalOnCompletion"; + } + } + + private final class RestConsumerBindingCORSOnCompletion extends SynchronizationAdapter { + + private final Map<String, String> corsHeaders; + + private RestConsumerBindingCORSOnCompletion(Map<String, String> corsHeaders) { + this.corsHeaders = corsHeaders; + } + + @Override + public void onAfterRoute(Route route, Exchange exchange) { + // add the CORS headers after routing, but before the consumer writes the response + Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); + + // use default value if none has been configured + String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null; + if (allowOrigin == null) { + allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN; + } + String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null; + if (allowMethods == null) { + allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS; + } + String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null; + if (allowHeaders == null) { + allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS; + } + String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null; + if (maxAge == null) { + maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE; + } + + msg.setHeader("Access-Control-Allow-Origin", allowOrigin); + msg.setHeader("Access-Control-Allow-Methods", allowMethods); + msg.setHeader("Access-Control-Allow-Headers", allowHeaders); + msg.setHeader("Access-Control-Max-Age", maxAge); + } + + @Override + public String toString() { + return "RestConsumerBindingCORSOnCompletion"; + } + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java b/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java index 04b4ced..29cde1d 100644 --- a/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java +++ b/camel-core/src/main/java/org/apache/camel/component/rest/RestEndpoint.java @@ -67,7 +67,7 @@ public class RestEndpoint extends DefaultEndpoint { private String inType; @UriParam(label = "common") private String outType; - @UriParam(label = "consumer") + @UriParam(label = "common") private String routeId; @UriParam(label = "consumer") private String description; @@ -326,7 +326,11 @@ public class RestEndpoint extends DefaultEndpoint { producer = factory.createProducer(getCamelContext(), host, method, path, uriTemplate, queryParameters, consumes, produces, parameters); } RestConfiguration config = getCamelContext().getRestConfiguration(cname, true); - return new RestProducer(this, producer, config); + RestProducer answer = new RestProducer(this, producer, config); + answer.setOutType(outType); + answer.setType(inType); + + return answer; } else { throw new IllegalStateException("Cannot find RestProducerFactory in Registry or as a Component to use"); } http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java b/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java index 97976d6..9ada4e4 100644 --- a/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java +++ b/camel-core/src/main/java/org/apache/camel/component/rest/RestProducer.java @@ -20,6 +20,8 @@ import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; +import javax.xml.bind.JAXBContext; + import org.apache.camel.AsyncCallback; import org.apache.camel.AsyncProcessor; import org.apache.camel.CamelContext; @@ -29,10 +31,11 @@ import org.apache.camel.Producer; import org.apache.camel.impl.DefaultAsyncProducer; import org.apache.camel.spi.DataFormat; import org.apache.camel.spi.RestConfiguration; -import org.apache.camel.tools.apt.helper.CollectionStringBuffer; import org.apache.camel.util.AsyncProcessorConverterHelper; +import org.apache.camel.util.CollectionStringBuffer; import org.apache.camel.util.EndpointHelper; import org.apache.camel.util.FileUtil; +import org.apache.camel.util.IntrospectionSupport; import org.apache.camel.util.ServiceHelper; import org.apache.camel.util.URISupport; @@ -43,15 +46,17 @@ public class RestProducer extends DefaultAsyncProducer { private final CamelContext camelContext; private final RestConfiguration configuration; + private boolean prepareUriTemplate = true; private String bindingMode; private Boolean skipBindingOnErrorCode; + private String type; + private String outType; // the producer of the Camel component that is used as the HTTP client to call the REST service private AsyncProcessor producer; + // if binding is enabled then this processor should be used to wrap the call with binding before/after private AsyncProcessor binding; - private boolean prepareUriTemplate = true; - public RestProducer(Endpoint endpoint, Producer producer, RestConfiguration configuration) { super(endpoint); this.camelContext = endpoint.getCamelContext(); @@ -61,12 +66,7 @@ public class RestProducer extends DefaultAsyncProducer { @Override public boolean process(Exchange exchange, AsyncCallback callback) { - // TODO: request bind to consumes context-type - // TODO: response bind to content-type returned in response - // TODO: binding // TODO: binding get type/outType from api-doc if possible - // TODO: binding reverse only enabled if outType configured - // TODO move consumer binding processor to this pacakge so they are both the same place try { prepareExchange(exchange); @@ -116,6 +116,22 @@ public class RestProducer extends DefaultAsyncProducer { this.skipBindingOnErrorCode = skipBindingOnErrorCode; } + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getOutType() { + return outType; + } + + public void setOutType(String outType) { + this.outType = outType; + } + protected void prepareExchange(Exchange exchange) throws Exception { boolean hasPath = false; @@ -249,7 +265,26 @@ public class RestProducer extends DefaultAsyncProducer { } if (json != null) { + Class<?> clazz = null; + if (type != null) { + String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type; + clazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); + } + if (clazz != null) { + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), json, "unmarshalType", clazz); + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), json, "useList", type.endsWith("[]")); + } setAdditionalConfiguration(configuration, camelContext, json, "json.in."); + + Class<?> outClazz = null; + if (outType != null) { + String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType; + outClazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); + } + if (outClazz != null) { + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJson, "unmarshalType", outClazz); + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJson, "useList", outType.endsWith("[]")); + } setAdditionalConfiguration(configuration, camelContext, outJson, "json.out."); } @@ -274,11 +309,34 @@ public class RestProducer extends DefaultAsyncProducer { } if (jaxb != null) { + Class<?> clazz = null; + if (type != null) { + String typeName = type.endsWith("[]") ? type.substring(0, type.length() - 2) : type; + clazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); + } + if (clazz != null) { + JAXBContext jc = JAXBContext.newInstance(clazz); + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), jaxb, "context", jc); + } setAdditionalConfiguration(configuration, camelContext, jaxb, "xml.in."); + + Class<?> outClazz = null; + if (outType != null) { + String typeName = outType.endsWith("[]") ? outType.substring(0, outType.length() - 2) : outType; + outClazz = camelContext.getClassResolver().resolveMandatoryClass(typeName); + } + if (outClazz != null) { + JAXBContext jc = JAXBContext.newInstance(outClazz); + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJaxb, "context", jc); + } else if (clazz != null) { + // fallback and use the context from the input + JAXBContext jc = JAXBContext.newInstance(clazz); + IntrospectionSupport.setProperty(camelContext.getTypeConverter(), outJaxb, "context", jc); + } setAdditionalConfiguration(configuration, camelContext, outJaxb, "xml.out."); } - return new RestProducerBindingProcessor(producer, camelContext, json, jaxb, outJson, outJaxb, mode, skip); + return new RestProducerBindingProcessor(producer, camelContext, json, jaxb, outJson, outJaxb, mode, skip, type, outType); } private void setAdditionalConfiguration(RestConfiguration config, CamelContext context, http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/java/org/apache/camel/component/rest/RestProducerBindingProcessor.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/component/rest/RestProducerBindingProcessor.java b/camel-core/src/main/java/org/apache/camel/component/rest/RestProducerBindingProcessor.java index f17001d..7c254e3 100644 --- a/camel-core/src/main/java/org/apache/camel/component/rest/RestProducerBindingProcessor.java +++ b/camel-core/src/main/java/org/apache/camel/component/rest/RestProducerBindingProcessor.java @@ -29,11 +29,17 @@ import org.apache.camel.processor.MarshalProcessor; import org.apache.camel.processor.UnmarshalProcessor; import org.apache.camel.processor.binding.BindingException; import org.apache.camel.spi.DataFormat; -import org.apache.camel.support.SynchronizationAdapter; import org.apache.camel.util.ExchangeHelper; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.ServiceHelper; +/** + * 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. + */ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { private final CamelContext camelContext; @@ -43,44 +49,49 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { private final AsyncProcessor xmlMarshal; private final String bindingMode; private final boolean skipBindingOnErrorCode; + private final String type; + private final String outType; public RestProducerBindingProcessor(AsyncProcessor processor, CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, - String bindingMode, boolean skipBindingOnErrorCode) { + String bindingMode, boolean skipBindingOnErrorCode, + String type, String outType) { super(processor); this.camelContext = camelContext; if (jsonDataFormat != null) { - this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat); + this.jsonUnmarshal = new UnmarshalProcessor(outJsonDataFormat); } else { this.jsonUnmarshal = null; } if (outJsonDataFormat != null) { - this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); - } else if (jsonDataFormat != null) { this.jsonMarshal = new MarshalProcessor(jsonDataFormat); + } else if (jsonDataFormat != null) { + this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); } else { this.jsonMarshal = null; } if (xmlDataFormat != null) { - this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat); + this.xmlUnmarshal = new UnmarshalProcessor(outXmlDataFormat); } else { this.xmlUnmarshal = null; } if (outXmlDataFormat != null) { - this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); - } else if (xmlDataFormat != null) { this.xmlMarshal = new MarshalProcessor(xmlDataFormat); + } else if (xmlDataFormat != null) { + this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); } else { this.xmlMarshal = null; } this.bindingMode = bindingMode; this.skipBindingOnErrorCode = skipBindingOnErrorCode; + this.type = type; + this.outType = outType; } @Override @@ -116,21 +127,26 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { boolean isXml = false; boolean isJson = false; - // skip binding for empty/null body + // skip before binding for empty/null body Object body = exchange.getIn().getBody(); if (ObjectHelper.isEmpty(body)) { - // TODO: add reverse operation to call before callback + if (outType != null) { + // wrap callback to add reverse operation if we know the output type from the REST service + callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false); + } // okay now we can continue routing to the producer return getProcessor().process(exchange, callback); } - // we only need to perform binding if the message body is POJO based + // we only need to perform before binding if the message body is POJO based // 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); - // add reverse operation - exchange.addOnCompletion(new RestProducerBindingUnmarshalOnCompletion(jsonMarshal, xmlMarshal, false)); + if (outType != null) { + // wrap callback to add reverse operation if we know the output type from the REST service + callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false); + } // okay now we can continue routing to the producer return getProcessor().process(exchange, callback); } @@ -153,7 +169,6 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { // favor json over xml if (isJson && jsonMarshal != null) { - // TODO: add reverse operation to call before callback try { jsonMarshal.process(exchange); } catch (Exception e) { @@ -164,12 +179,13 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { } // need to prepare exchange first ExchangeHelper.prepareOutToIn(exchange); - // add reverse operation - exchange.addOnCompletion(new RestProducerBindingUnmarshalOnCompletion(jsonMarshal, xmlMarshal, false)); + if (outType != null) { + // wrap callback to add reverse operation if we know the output type from the REST service + callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false); + } // okay now we can continue routing to the producer return getProcessor().process(exchange, callback); } else if (isXml && xmlMarshal != null) { - // TODO: add reverse operation to call before callback try { xmlMarshal.process(exchange); } catch (Exception e) { @@ -180,16 +196,20 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { } // need to prepare exchange first ExchangeHelper.prepareOutToIn(exchange); - // add reverse operation - exchange.addOnCompletion(new RestProducerBindingUnmarshalOnCompletion(jsonMarshal, xmlMarshal, false)); + if (outType != null) { + // wrap callback to add reverse operation if we know the output type from the REST service + callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, 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")) { - // add reverse operation - exchange.addOnCompletion(new RestProducerBindingUnmarshalOnCompletion(jsonMarshal, xmlMarshal, false)); + if (outType != null) { + // wrap callback to add reverse operation if we know the output type from the REST service + callback = new RestProducerBindingUnmarshalCallback(exchange, callback, jsonMarshal, xmlMarshal, false); + } // okay now we can continue routing to the producer return getProcessor().process(exchange, callback); } else { @@ -204,20 +224,36 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { } } - private final class RestProducerBindingUnmarshalOnCompletion extends SynchronizationAdapter { + private final class RestProducerBindingUnmarshalCallback implements AsyncCallback { + private final Exchange exchange; + private final AsyncCallback callback; private final AsyncProcessor jsonMarshal; private final AsyncProcessor xmlMarshal; private boolean wasXml; - private RestProducerBindingUnmarshalOnCompletion(AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml) { + private RestProducerBindingUnmarshalCallback(Exchange exchange, AsyncCallback callback, + AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml) { + this.exchange = exchange; + this.callback = callback; this.jsonMarshal = jsonMarshal; this.xmlMarshal = xmlMarshal; this.wasXml = wasXml; } @Override - public void onDone(Exchange exchange) { + public void done(boolean doneSync) { + try { + doDone(); + } catch (Throwable 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; @@ -234,20 +270,11 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { boolean isXml = false; boolean isJson = false; - // fallback to content type if still undecided - if (!isXml && !isJson) { - String contentType = ExchangeHelper.getContentType(exchange); - if (contentType != null) { - isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); - } - } - // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with - // that information in the consumes - if (!isXml && !isJson) { - // TODO: -// isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml"); -// isJson = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("json"); + // 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) @@ -272,8 +299,7 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { ExchangeHelper.prepareOutToIn(exchange); // ensure there is a content type header (even if binding is off) - // TODO: - // ensureHeaderContentType(produces, isXml, isJson, exchange); + ensureHeaderContentType(isXml, isJson, exchange); if (bindingMode == null || "off".equals(bindingMode)) { // binding is off, so no message body binding @@ -290,7 +316,7 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { return; } - String contentType = exchange.getIn().getHeader(Exchange.CONTENT_TYPE, String.class); + contentType = exchange.getIn().getHeader(Exchange.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 { @@ -311,9 +337,9 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { // okay for auto we do not mind if we could not bind } else { if (bindingMode.contains("xml")) { - exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); + exchange.setException(new BindingException("Cannot bind from xml as message body is not xml compatible", exchange)); } else { - exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); + exchange.setException(new BindingException("Cannot bind from json as message body is not json compatible", exchange)); } } } @@ -322,15 +348,7 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { } } - private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) { - // favor given content type - if (contentType != null) { - String type = ExchangeHelper.getContentType(exchange); - if (type == null) { - exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType); - } - } - + private void ensureHeaderContentType(boolean isXml, boolean isJson, Exchange exchange) { // favor json over xml if (isJson) { // make sure there is a content-type with json @@ -349,7 +367,7 @@ public class RestProducerBindingProcessor extends DelegateAsyncProcessor { @Override public String toString() { - return "RestProducerBindingUnmarshalOnCompletion"; + return "RestProducerBindingUnmarshalCallback"; } } http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java b/camel-core/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java index e943d2d..8aeef30 100644 --- a/camel-core/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java +++ b/camel-core/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java @@ -28,7 +28,7 @@ import javax.xml.bind.annotation.XmlTransient; import org.apache.camel.CamelContext; import org.apache.camel.Processor; import org.apache.camel.model.NoOutputDefinition; -import org.apache.camel.processor.binding.RestConsumerBindingProcessor; +import org.apache.camel.component.rest.RestConsumerBindingProcessor; import org.apache.camel.spi.DataFormat; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.RestConfiguration; http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/camel-core/src/main/java/org/apache/camel/processor/binding/RestConsumerBindingProcessor.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/processor/binding/RestConsumerBindingProcessor.java b/camel-core/src/main/java/org/apache/camel/processor/binding/RestConsumerBindingProcessor.java deleted file mode 100644 index d9f0656..0000000 --- a/camel-core/src/main/java/org/apache/camel/processor/binding/RestConsumerBindingProcessor.java +++ /dev/null @@ -1,460 +0,0 @@ -/** - * 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.processor.binding; - -import java.util.Locale; -import java.util.Map; - -import org.apache.camel.AsyncCallback; -import org.apache.camel.AsyncProcessor; -import org.apache.camel.CamelContext; -import org.apache.camel.CamelContextAware; -import org.apache.camel.Exchange; -import org.apache.camel.Message; -import org.apache.camel.Route; -import org.apache.camel.processor.MarshalProcessor; -import org.apache.camel.processor.UnmarshalProcessor; -import org.apache.camel.spi.DataFormat; -import org.apache.camel.spi.RestConfiguration; -import org.apache.camel.support.ServiceSupport; -import org.apache.camel.support.SynchronizationAdapter; -import org.apache.camel.util.AsyncProcessorHelper; -import org.apache.camel.util.ExchangeHelper; -import org.apache.camel.util.MessageHelper; -import org.apache.camel.util.ObjectHelper; -import org.apache.camel.util.ServiceHelper; - -/** - * A {@link org.apache.camel.Processor} that binds the REST DSL incoming and outgoing 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. - */ -public class RestConsumerBindingProcessor extends ServiceSupport implements AsyncProcessor { - - private final CamelContext camelContext; - private final AsyncProcessor jsonUnmarshal; - private final AsyncProcessor xmlUnmarshal; - private final AsyncProcessor jsonMarshal; - private final AsyncProcessor xmlMarshal; - private final String consumes; - private final String produces; - private final String bindingMode; - private final boolean skipBindingOnErrorCode; - private final boolean enableCORS; - private final Map<String, String> corsHeaders; - private final Map<String, String> queryDefaultValues; - - public RestConsumerBindingProcessor(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, - DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, - String consumes, String produces, String bindingMode, - boolean skipBindingOnErrorCode, boolean enableCORS, - Map<String, String> corsHeaders, - Map<String, String> queryDefaultValues) { - - this.camelContext = camelContext; - - if (jsonDataFormat != null) { - this.jsonUnmarshal = new UnmarshalProcessor(jsonDataFormat); - } else { - this.jsonUnmarshal = null; - } - if (outJsonDataFormat != null) { - this.jsonMarshal = new MarshalProcessor(outJsonDataFormat); - } else if (jsonDataFormat != null) { - this.jsonMarshal = new MarshalProcessor(jsonDataFormat); - } else { - this.jsonMarshal = null; - } - - if (xmlDataFormat != null) { - this.xmlUnmarshal = new UnmarshalProcessor(xmlDataFormat); - } else { - this.xmlUnmarshal = null; - } - if (outXmlDataFormat != null) { - this.xmlMarshal = new MarshalProcessor(outXmlDataFormat); - } else if (xmlDataFormat != null) { - this.xmlMarshal = new MarshalProcessor(xmlDataFormat); - } else { - this.xmlMarshal = null; - } - - this.consumes = consumes; - this.produces = produces; - this.bindingMode = bindingMode; - this.skipBindingOnErrorCode = skipBindingOnErrorCode; - this.enableCORS = enableCORS; - this.corsHeaders = corsHeaders; - this.queryDefaultValues = queryDefaultValues; - } - - @Override - public void process(Exchange exchange) throws Exception { - AsyncProcessorHelper.process(this, exchange); - } - - @Override - public boolean process(Exchange exchange, final AsyncCallback callback) { - if (enableCORS) { - exchange.addOnCompletion(new RestBindingCORSOnCompletion(corsHeaders)); - } - - String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class); - if ("OPTIONS".equalsIgnoreCase(method)) { - // for OPTIONS methods then we should not route at all as its part of CORS - exchange.setProperty(Exchange.ROUTE_STOP, true); - callback.done(true); - return true; - } - - boolean isXml = false; - boolean isJson = false; - - String contentType = ExchangeHelper.getContentType(exchange); - if (contentType != null) { - isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); - } - // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with - // that information in the consumes - if (!isXml && !isJson) { - isXml = consumes != null && consumes.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = consumes != null && consumes.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"); - } - - String accept = exchange.getIn().getHeader("Accept", String.class); - - String body = null; - if (exchange.getIn().getBody() != null) { - - // okay we have a binding mode, so need to check for empty body as that can cause the marshaller to fail - // as they assume a non-empty body - if (isXml || isJson) { - // we have binding enabled, so we need to know if there body is empty or not - // so force reading the body as a String which we can work with - body = MessageHelper.extractBodyAsString(exchange.getIn()); - if (body != null) { - exchange.getIn().setBody(body); - - if (isXml && isJson) { - // we have still not determined between xml or json, so check the body if its xml based or not - isXml = body.startsWith("<"); - isJson = !isXml; - } - } - } - } - - // add missing default values which are mapped as headers - if (queryDefaultValues != null) { - for (Map.Entry<String, String> entry : queryDefaultValues.entrySet()) { - if (exchange.getIn().getHeader(entry.getKey()) == null) { - exchange.getIn().setHeader(entry.getKey(), entry.getValue()); - } - } - } - - // favor json over xml - if (isJson && jsonUnmarshal != null) { - // add reverse operation - exchange.addOnCompletion(new RestBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept)); - if (ObjectHelper.isNotEmpty(body)) { - return jsonUnmarshal.process(exchange, callback); - } else { - callback.done(true); - return true; - } - } else if (isXml && xmlUnmarshal != null) { - // add reverse operation - exchange.addOnCompletion(new RestBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, true, accept)); - if (ObjectHelper.isNotEmpty(body)) { - return xmlUnmarshal.process(exchange, callback); - } else { - callback.done(true); - return true; - } - } - - // we could not bind - if ("off".equals(bindingMode) || bindingMode.equals("auto")) { - // okay for auto we do not mind if we could not bind - exchange.addOnCompletion(new RestBindingMarshalOnCompletion(exchange.getFromRouteId(), jsonMarshal, xmlMarshal, false, accept)); - callback.done(true); - return true; - } else { - if (bindingMode.contains("xml")) { - exchange.setException(new BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); - } else { - exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); - } - callback.done(true); - return true; - } - } - - @Override - public String toString() { - return "RestBindingProcessor"; - } - - @Override - protected void doStart() throws Exception { - // inject CamelContext before starting - if (jsonMarshal instanceof CamelContextAware) { - ((CamelContextAware) jsonMarshal).setCamelContext(camelContext); - } - if (jsonUnmarshal instanceof CamelContextAware) { - ((CamelContextAware) jsonUnmarshal).setCamelContext(camelContext); - } - if (xmlMarshal instanceof CamelContextAware) { - ((CamelContextAware) xmlMarshal).setCamelContext(camelContext); - } - if (xmlUnmarshal instanceof CamelContextAware) { - ((CamelContextAware) xmlUnmarshal).setCamelContext(camelContext); - } - ServiceHelper.startServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal); - } - - @Override - protected void doStop() throws Exception { - ServiceHelper.stopServices(jsonMarshal, jsonUnmarshal, xmlMarshal, xmlUnmarshal); - } - - /** - * An {@link org.apache.camel.spi.Synchronization} that does the reverse operation - * of marshalling from POJO to json/xml - */ - private final class RestBindingMarshalOnCompletion extends SynchronizationAdapter { - - private final AsyncProcessor jsonMarshal; - private final AsyncProcessor xmlMarshal; - private final String routeId; - private boolean wasXml; - private String accept; - - private RestBindingMarshalOnCompletion(String routeId, AsyncProcessor jsonMarshal, AsyncProcessor xmlMarshal, boolean wasXml, String accept) { - this.routeId = routeId; - this.jsonMarshal = jsonMarshal; - this.xmlMarshal = xmlMarshal; - this.wasXml = wasXml; - this.accept = accept; - } - - @Override - public void onAfterRoute(Route route, Exchange exchange) { - // we use the onAfterRoute callback, to ensure the data has been marshalled before - // the consumer writes the response back - - // only trigger when it was the 1st route that was done - if (!routeId.equals(route.getId())) { - return; - } - - // only marshal if there was no exception - if (exchange.getException() != null) { - return; - } - - if (skipBindingOnErrorCode) { - Integer code = exchange.hasOut() ? exchange.getOut().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class) : exchange.getIn().getHeader(Exchange.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; - - // accept takes precedence - if (accept != null) { - isXml = accept.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = accept.toLowerCase(Locale.ENGLISH).contains("json"); - } - // fallback to content type if still undecided - if (!isXml && !isJson) { - String contentType = ExchangeHelper.getContentType(exchange); - if (contentType != null) { - isXml = contentType.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = contentType.toLowerCase(Locale.ENGLISH).contains("json"); - } - } - // if content type could not tell us if it was json or xml, then fallback to if the binding was configured with - // that information in the consumes - if (!isXml && !isJson) { - isXml = produces != null && produces.toLowerCase(Locale.ENGLISH).contains("xml"); - isJson = produces != null && produces.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(produces, isXml, isJson, exchange); - - if (bindingMode == null || "off".equals(bindingMode)) { - // binding is off, so no message body binding - return; - } - - // is there any marshaller at all - if (jsonMarshal == null && xmlMarshal == null) { - return; - } - - // is the body empty - if ((exchange.hasOut() && exchange.getOut().getBody() == null) || (!exchange.hasOut() && exchange.getIn().getBody() == null)) { - return; - } - - String contentType = exchange.getIn().getHeader(Exchange.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 && jsonMarshal != null) { - // only marshal if its json content type - if (contentType.contains("json")) { - jsonMarshal.process(exchange); - } - } else if (isXml && xmlMarshal != null) { - // only marshal if its xml content type - if (contentType.contains("xml")) { - xmlMarshal.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 BindingException("Cannot bind to xml as message body is not xml compatible", exchange)); - } else { - exchange.setException(new BindingException("Cannot bind to json as message body is not json compatible", exchange)); - } - } - } - } catch (Throwable e) { - exchange.setException(e); - } - } - - private void ensureHeaderContentType(String contentType, boolean isXml, boolean isJson, Exchange exchange) { - // favor given content type - if (contentType != null) { - String type = ExchangeHelper.getContentType(exchange); - if (type == null) { - exchange.getIn().setHeader(Exchange.CONTENT_TYPE, contentType); - } - } - - // 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(Exchange.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(Exchange.CONTENT_TYPE, "application/xml"); - } - } - } - - @Override - public String toString() { - return "RestBindingMarshalOnCompletion"; - } - } - - private final class RestBindingCORSOnCompletion extends SynchronizationAdapter { - - private final Map<String, String> corsHeaders; - - private RestBindingCORSOnCompletion(Map<String, String> corsHeaders) { - this.corsHeaders = corsHeaders; - } - - @Override - public void onAfterRoute(Route route, Exchange exchange) { - // add the CORS headers after routing, but before the consumer writes the response - Message msg = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); - - // use default value if none has been configured - String allowOrigin = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Origin") : null; - if (allowOrigin == null) { - allowOrigin = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_ORIGIN; - } - String allowMethods = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Methods") : null; - if (allowMethods == null) { - allowMethods = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_METHODS; - } - String allowHeaders = corsHeaders != null ? corsHeaders.get("Access-Control-Allow-Headers") : null; - if (allowHeaders == null) { - allowHeaders = RestConfiguration.CORS_ACCESS_CONTROL_ALLOW_HEADERS; - } - String maxAge = corsHeaders != null ? corsHeaders.get("Access-Control-Max-Age") : null; - if (maxAge == null) { - maxAge = RestConfiguration.CORS_ACCESS_CONTROL_MAX_AGE; - } - - msg.setHeader("Access-Control-Allow-Origin", allowOrigin); - msg.setHeader("Access-Control-Allow-Methods", allowMethods); - msg.setHeader("Access-Control-Allow-Headers", allowHeaders); - msg.setHeader("Access-Control-Max-Age", maxAge); - } - - @Override - public String toString() { - return "RestBindingCORSOnCompletion"; - } - } - -} http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerPojoInOutTest.java ---------------------------------------------------------------------- diff --git a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerPojoInOutTest.java b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerPojoInOutTest.java index bd4f2bf..a0fae86 100644 --- a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerPojoInOutTest.java +++ b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/producer/JettyRestProducerPojoInOutTest.java @@ -69,7 +69,8 @@ public class JettyRestProducerPojoInOutTest extends BaseJettyTest { user.setId(123); user.setName("Donald Duck"); - CountryPojo pojo = fluentTemplate.to("rest:post:users/lives") + // must provide outType parameter to tell Camel to bind the output from the REST service from json to POJO + CountryPojo pojo = fluentTemplate.to("rest:post:users/lives?outType=org.apache.camel.component.jetty.rest.CountryPojo") .withHeader(Exchange.CONTENT_TYPE, "application/json") .withBody(user).request(CountryPojo.class); @@ -90,10 +91,9 @@ public class JettyRestProducerPojoInOutTest extends BaseJettyTest { // use the rest DSL to define the rest services rest("/users/") // just return the default country here - .get("lives").to("direct:start") - .post("lives").type(UserPojo.class).outType(CountryPojo.class) + .get("lives").to("direct:start") + .post("lives").type(UserPojo.class).outType(CountryPojo.class) .route() - .log("Lives where") .bean(new UserService(), "livesWhere"); CountryPojo country = new CountryPojo(); http://git-wip-us.apache.org/repos/asf/camel/blob/29cd3cdb/components/camel-jetty9/src/test/resources/log4j2.properties ---------------------------------------------------------------------- diff --git a/components/camel-jetty9/src/test/resources/log4j2.properties b/components/camel-jetty9/src/test/resources/log4j2.properties index d54c30d..c863dc3 100644 --- a/components/camel-jetty9/src/test/resources/log4j2.properties +++ b/components/camel-jetty9/src/test/resources/log4j2.properties @@ -25,4 +25,4 @@ appender.out.name = out appender.out.layout.type = PatternLayout appender.out.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m%n rootLogger.level = INFO -rootLogger.appenderRef.out.ref = out +rootLogger.appenderRef.file.ref = file