From 25b2ba250776c0046d0bcec869f0aa98caa92e4c Mon Sep 17 00:00:00 2001 From: Tomohisa Igarashi Date: Fri, 3 Feb 2017 15:17:23 +0900 Subject: [PATCH 1/2] CAMEL-10532 Extract ContractAdvice as an individual file --- .../camel/impl/DefaultRouteContext.java | 3 +- .../processor/CamelInternalProcessor.java | 140 ---------------- .../camel/processor/ContractAdvice.java | 152 ++++++++++++++++++ 3 files changed, 154 insertions(+), 141 deletions(-) create mode 100644 camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java diff --git a/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java b/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java index 8a2e7c1bc57ab..328dae9c81241 100644 --- a/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java +++ b/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java @@ -35,6 +35,7 @@ import org.apache.camel.model.ProcessorDefinition; import org.apache.camel.model.RouteDefinition; import org.apache.camel.processor.CamelInternalProcessor; +import org.apache.camel.processor.ContractAdvice; import org.apache.camel.processor.Pipeline; import org.apache.camel.spi.Contract; import org.apache.camel.spi.InterceptStrategy; @@ -195,7 +196,7 @@ public void commit() { // wrap in contract if (contract != null) { - internal.addAdvice(new CamelInternalProcessor.ContractAdvice(contract)); + internal.addAdvice(new ContractAdvice(contract)); } // and create the route that wraps the UoW diff --git a/camel-core/src/main/java/org/apache/camel/processor/CamelInternalProcessor.java b/camel-core/src/main/java/org/apache/camel/processor/CamelInternalProcessor.java index 3ce9220958f73..c98b1b043d9bd 100644 --- a/camel-core/src/main/java/org/apache/camel/processor/CamelInternalProcessor.java +++ b/camel-core/src/main/java/org/apache/camel/processor/CamelInternalProcessor.java @@ -25,7 +25,6 @@ import org.apache.camel.AsyncCallback; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; -import org.apache.camel.Message; import org.apache.camel.MessageHistory; import org.apache.camel.Ordered; import org.apache.camel.Processor; @@ -40,8 +39,6 @@ import org.apache.camel.processor.interceptor.BacklogDebugger; import org.apache.camel.processor.interceptor.BacklogTracer; import org.apache.camel.processor.interceptor.DefaultBacklogTracerEventMessage; -import org.apache.camel.spi.Contract; -import org.apache.camel.spi.DataType; import org.apache.camel.spi.InflightRepository; import org.apache.camel.spi.MessageHistoryFactory; import org.apache.camel.spi.RouteContext; @@ -857,141 +854,4 @@ public void after(Exchange exchange, Object data) throws Exception { // noop } } - - /** - * Advice for data type contract - * TODO add declarative validation - */ - public static class ContractAdvice implements CamelInternalProcessorAdvice { - private Contract contract; - - public ContractAdvice(Contract contract) { - this.contract = contract; - } - - @Override - public Object before(Exchange exchange) throws Exception { - DataType from = getCurrentType(exchange, Exchange.INPUT_TYPE); - DataType to = contract.getInputType(); - if (to != null && !to.equals(from)) { - LOG.debug("Looking for transformer for INPUT: from='{}', to='{}'", from, to); - doTransform(exchange.getIn(), from, to); - exchange.setProperty(Exchange.INPUT_TYPE, to); - } - return null; - } - - @Override - public void after(Exchange exchange, Object data) throws Exception { - Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); - DataType from = getCurrentType(exchange, exchange.hasOut() ? Exchange.OUTPUT_TYPE : Exchange.INPUT_TYPE); - DataType to = contract.getOutputType(); - if (to != null && !to.equals(from)) { - LOG.debug("Looking for transformer for OUTPUT: from='{}', to='{}'", from, to); - doTransform(target, from, to); - exchange.setProperty(exchange.hasOut() ? Exchange.OUTPUT_TYPE : Exchange.INPUT_TYPE, to); - } - } - - private void doTransform(Message message, DataType from, DataType to) throws Exception { - // transform into 'from' type before performing declared transformation - convertIfRequired(message, from); - - if (applyExactlyMatchedTransformer(message, from, to)) { - // Found exactly matched transformer. Java-Java transformer is also allowed. - return; - } else if (from == null || from.isJavaType()) { - if (convertIfRequired(message, to)) { - // Java->Java transformation just relies on TypeConverter if no explicit transformer - return; - } else if (from == null) { - // {undefined}->Other transformation - assuming it's already in expected shape - return; - } else if (applyTransformerByToModel(message, from, to)) { - // Java->Other transformation - found a transformer supports 'to' data model - return; - } - } else if (from != null) { - if (to.isJavaType()) { - if (applyTransformerByFromModel(message, from, to)) { - // Other->Java transformation - found a transformer supprts 'from' data model - return; - } - } else if (applyTransformerChain(message, from, to)) { - // Other->Other transformation - found a transformer chain - return; - } - } - - throw new IllegalArgumentException("No Transformer found for [from='" + from + "', to='" + to + "']"); - } - - private boolean convertIfRequired(Message message, DataType type) throws Exception { - // TODO for better performance it may be better to add TypeConveterTransformer - // into transformer registry automatically to avoid unnecessary scan in transformer registry - CamelContext context = message.getExchange().getContext(); - if (type != null && type.isJavaType() && type.getName() != null) { - Class typeJava = getClazz(type.getName(), context); - if (!typeJava.isAssignableFrom(message.getBody().getClass())) { - LOG.debug("Converting to '{}'", typeJava.getName()); - message.setBody(message.getMandatoryBody(typeJava)); - return true; - } - } - return false; - } - - private boolean applyTransformer(Transformer transformer, Message message, DataType from, DataType to) throws Exception { - if (transformer != null) { - LOG.debug("Applying transformer: from='{}', to='{}', transformer='{}'", from, to, transformer); - transformer.transform(message, from, to); - return true; - } - return false; - } - private boolean applyExactlyMatchedTransformer(Message message, DataType from, DataType to) throws Exception { - Transformer transformer = message.getExchange().getContext().resolveTransformer(from, to); - return applyTransformer(transformer, message, from, to); - } - - private boolean applyTransformerByToModel(Message message, DataType from, DataType to) throws Exception { - Transformer transformer = message.getExchange().getContext().resolveTransformer(to.getModel()); - return applyTransformer(transformer, message, from, to); - } - - private boolean applyTransformerByFromModel(Message message, DataType from, DataType to) throws Exception { - Transformer transformer = message.getExchange().getContext().resolveTransformer(from.getModel()); - return applyTransformer(transformer, message, from, to); - } - - private boolean applyTransformerChain(Message message, DataType from, DataType to) throws Exception { - CamelContext context = message.getExchange().getContext(); - Transformer fromTransformer = context.resolveTransformer(from.getModel()); - Transformer toTransformer = context.resolveTransformer(to.getModel()); - if (fromTransformer != null && toTransformer != null) { - LOG.debug("Applying transformer 1/2: from='{}', to='{}', transformer='{}'", from, to, fromTransformer); - fromTransformer.transform(message, from, new DataType(Object.class)); - LOG.debug("Applying transformer 2/2: from='{}', to='{}', transformer='{}'", from, to, toTransformer); - toTransformer.transform(message, new DataType(Object.class), to); - return true; - } - return false; - } - - private Class getClazz(String type, CamelContext context) throws Exception { - return context.getClassResolver().resolveMandatoryClass(type); - } - - private DataType getCurrentType(Exchange exchange, String name) { - Object prop = exchange.getProperty(name); - if (prop instanceof DataType) { - return (DataType)prop; - } else if (prop instanceof String) { - DataType answer = new DataType((String)prop); - exchange.setProperty(name, answer); - return answer; - } - return null; - } - } } diff --git a/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java b/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java new file mode 100644 index 0000000000000..15a3d61db9099 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java @@ -0,0 +1,152 @@ +package org.apache.camel.processor; + +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.spi.Contract; +import org.apache.camel.spi.DataType; +import org.apache.camel.spi.Transformer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@code CamelInternalProcessorAdvice} which performs Transformation and Validation + * according to the data type Contract. + * + * TODO add declarative validation + * @see CamelInternalProcessor, CamelInternalProcessorAdvice + */ +public class ContractAdvice implements CamelInternalProcessorAdvice { + private static final Logger LOG = LoggerFactory.getLogger(CamelInternalProcessor.class); + + private Contract contract; + + public ContractAdvice(Contract contract) { + this.contract = contract; + } + + @Override + public Object before(Exchange exchange) throws Exception { + DataType from = getCurrentType(exchange, Exchange.INPUT_TYPE); + DataType to = contract.getInputType(); + if (to != null && !to.equals(from)) { + LOG.debug("Looking for transformer for INPUT: from='{}', to='{}'", from, to); + doTransform(exchange.getIn(), from, to); + exchange.setProperty(Exchange.INPUT_TYPE, to); + } + return null; + } + + @Override + public void after(Exchange exchange, Object data) throws Exception { + Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); + DataType from = getCurrentType(exchange, exchange.hasOut() ? Exchange.OUTPUT_TYPE : Exchange.INPUT_TYPE); + DataType to = contract.getOutputType(); + if (to != null && !to.equals(from)) { + LOG.debug("Looking for transformer for OUTPUT: from='{}', to='{}'", from, to); + doTransform(target, from, to); + exchange.setProperty(exchange.hasOut() ? Exchange.OUTPUT_TYPE : Exchange.INPUT_TYPE, to); + } + } + + private void doTransform(Message message, DataType from, DataType to) throws Exception { + // transform into 'from' type before performing declared transformation + convertIfRequired(message, from); + + if (applyExactlyMatchedTransformer(message, from, to)) { + // Found exactly matched transformer. Java-Java transformer is also allowed. + return; + } else if (from == null || from.isJavaType()) { + if (convertIfRequired(message, to)) { + // Java->Java transformation just relies on TypeConverter if no explicit transformer + return; + } else if (from == null) { + // {undefined}->Other transformation - assuming it's already in expected shape + return; + } else if (applyTransformerByToModel(message, from, to)) { + // Java->Other transformation - found a transformer supports 'to' data model + return; + } + } else if (from != null) { + if (to.isJavaType()) { + if (applyTransformerByFromModel(message, from, to)) { + // Other->Java transformation - found a transformer supprts 'from' data model + return; + } + } else if (applyTransformerChain(message, from, to)) { + // Other->Other transformation - found a transformer chain + return; + } + } + + throw new IllegalArgumentException("No Transformer found for [from='" + from + "', to='" + to + "']"); + } + + private boolean convertIfRequired(Message message, DataType type) throws Exception { + // TODO for better performance it may be better to add TypeConveterTransformer + // into transformer registry automatically to avoid unnecessary scan in transformer registry + CamelContext context = message.getExchange().getContext(); + if (type != null && type.isJavaType() && type.getName() != null) { + Class typeJava = getClazz(type.getName(), context); + if (!typeJava.isAssignableFrom(message.getBody().getClass())) { + LOG.debug("Converting to '{}'", typeJava.getName()); + message.setBody(message.getMandatoryBody(typeJava)); + return true; + } + } + return false; + } + + private boolean applyTransformer(Transformer transformer, Message message, DataType from, DataType to) throws Exception { + if (transformer != null) { + LOG.debug("Applying transformer: from='{}', to='{}', transformer='{}'", from, to, transformer); + transformer.transform(message, from, to); + return true; + } + return false; + } + private boolean applyExactlyMatchedTransformer(Message message, DataType from, DataType to) throws Exception { + Transformer transformer = message.getExchange().getContext().resolveTransformer(from, to); + return applyTransformer(transformer, message, from, to); + } + + private boolean applyTransformerByToModel(Message message, DataType from, DataType to) throws Exception { + Transformer transformer = message.getExchange().getContext().resolveTransformer(to.getModel()); + return applyTransformer(transformer, message, from, to); + } + + private boolean applyTransformerByFromModel(Message message, DataType from, DataType to) throws Exception { + Transformer transformer = message.getExchange().getContext().resolveTransformer(from.getModel()); + return applyTransformer(transformer, message, from, to); + } + + private boolean applyTransformerChain(Message message, DataType from, DataType to) throws Exception { + CamelContext context = message.getExchange().getContext(); + Transformer fromTransformer = context.resolveTransformer(from.getModel()); + Transformer toTransformer = context.resolveTransformer(to.getModel()); + if (fromTransformer != null && toTransformer != null) { + LOG.debug("Applying transformer 1/2: from='{}', to='{}', transformer='{}'", from, to, fromTransformer); + fromTransformer.transform(message, from, new DataType(Object.class)); + LOG.debug("Applying transformer 2/2: from='{}', to='{}', transformer='{}'", from, to, toTransformer); + toTransformer.transform(message, new DataType(Object.class), to); + return true; + } + return false; + } + + private Class getClazz(String type, CamelContext context) throws Exception { + return context.getClassResolver().resolveMandatoryClass(type); + } + + private DataType getCurrentType(Exchange exchange, String name) { + Object prop = exchange.getProperty(name); + if (prop instanceof DataType) { + return (DataType)prop; + } else if (prop instanceof String) { + DataType answer = new DataType((String)prop); + exchange.setProperty(name, answer); + return answer; + } + return null; + } +} \ No newline at end of file From 8c9184b6f2d75ebbab90a5cb42eb51e114fec42b Mon Sep 17 00:00:00 2001 From: Tomohisa Igarashi Date: Thu, 9 Feb 2017 17:48:16 +0900 Subject: [PATCH 2/2] CAMEL-10532 Convert RestConsumerBindingProcessor into processor advice so it works fine with contract advice together --- .../rest/RestConsumerBindingProcessor.java | 461 ------------------ .../camel/impl/DefaultRouteContext.java | 23 +- .../apache/camel/model/RouteDefinition.java | 23 +- .../model/rest/RestBindingDefinition.java | 18 +- .../camel/model/rest/RestDefinition.java | 2 +- .../camel/processor/ContractAdvice.java | 34 +- .../camel/processor/RestBindingAdvice.java | 418 ++++++++++++++++ .../java/org/apache/camel/spi/DataType.java | 2 +- .../org/apache/camel/spi/RouteContext.java | 5 - .../rest/FromRestGetEmbeddedRouteTest.java | 4 +- .../rest/FromRestGetEndPathTest.java | 2 +- .../rest/FromRestGetInterceptTest.java | 2 +- .../ManagedTransformerRegistryTest.java | 4 +- ...tJettyBindingModeJsonWithContractTest.java | 92 ++++ ...stJettyBindingModeOffWithContractTest.java | 99 ++++ .../component/jetty/rest/UserPojoEx.java | 48 ++ ...tyHttpBindingModeJsonWithContractTest.java | 83 ++++ ...ttyHttpBindingModeOffWithContractTest.java | 90 ++++ .../netty4/http/rest/UserPojoEx.java | 48 ++ ...estletBindingModeJsonWithContractTest.java | 91 ++++ ...RestletBindingModeOffWithContractTest.java | 98 ++++ ...estRestletCustomDataFormatInvalidTest.java | 2 +- .../camel/component/restlet/UserPojoEx.java | 48 ++ ...ervletBindingModeJsonWithContractTest.java | 92 ++++ ...ServletBindingModeOffWithContractTest.java | 99 ++++ .../component/servlet/rest/UserPojoEx.java | 48 ++ .../rest/FromRestGetEmbeddedRouteTest.java | 4 +- .../rest/FromRestGetInterceptTest.java | 2 +- ...owHttpBindingModeJsonWithContractTest.java | 83 ++++ ...towHttpBindingModeOffWithContractTest.java | 90 ++++ .../component/undertow/rest/UserPojoEx.java | 48 ++ 31 files changed, 1657 insertions(+), 506 deletions(-) delete mode 100644 camel-core/src/main/java/org/apache/camel/component/rest/RestConsumerBindingProcessor.java create mode 100644 camel-core/src/main/java/org/apache/camel/processor/RestBindingAdvice.java create mode 100644 components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeJsonWithContractTest.java create mode 100644 components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeOffWithContractTest.java create mode 100644 components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/UserPojoEx.java create mode 100644 components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeJsonWithContractTest.java create mode 100644 components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeOffWithContractTest.java create mode 100644 components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/UserPojoEx.java create mode 100644 components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeJsonWithContractTest.java create mode 100644 components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeOffWithContractTest.java create mode 100644 components/camel-restlet/src/test/java/org/apache/camel/component/restlet/UserPojoEx.java create mode 100644 components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeJsonWithContractTest.java create mode 100644 components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeOffWithContractTest.java create mode 100644 components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/UserPojoEx.java create mode 100644 components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeJsonWithContractTest.java create mode 100644 components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeOffWithContractTest.java create mode 100644 components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/UserPojoEx.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 deleted file mode 100644 index 888fc2fde7233..0000000000000 --- a/camel-core/src/main/java/org/apache/camel/component/rest/RestConsumerBindingProcessor.java +++ /dev/null @@ -1,461 +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.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. - *

- * 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 corsHeaders; - private final Map queryDefaultValues; - - public RestConsumerBindingProcessor(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, - DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, - String consumes, String produces, String bindingMode, - boolean skipBindingOnErrorCode, boolean enableCORS, - Map corsHeaders, - Map 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 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 corsHeaders; - - private RestConsumerBindingCORSOnCompletion(Map 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"; - } - } - -} diff --git a/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java b/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java index 328dae9c81241..fbb3673af4dbb 100644 --- a/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java +++ b/camel-core/src/main/java/org/apache/camel/impl/DefaultRouteContext.java @@ -37,6 +37,7 @@ import org.apache.camel.processor.CamelInternalProcessor; import org.apache.camel.processor.ContractAdvice; import org.apache.camel.processor.Pipeline; +import org.apache.camel.processor.RestBindingAdvice; import org.apache.camel.spi.Contract; import org.apache.camel.spi.InterceptStrategy; import org.apache.camel.spi.RouteContext; @@ -70,7 +71,6 @@ public class DefaultRouteContext implements RouteContext { private List routePolicyList = new ArrayList(); private ShutdownRoute shutdownRoute; private ShutdownRunningTask shutdownRunningTask; - private Contract contract; public DefaultRouteContext(CamelContext camelContext, RouteDefinition route, FromDefinition from, Collection routes) { this.camelContext = camelContext; @@ -194,8 +194,24 @@ public void commit() { // wrap in route lifecycle internal.addAdvice(new CamelInternalProcessor.RouteLifecycleAdvice()); + // wrap in REST binding + if (route.getRestBindingDefinition() != null) { + try { + internal.addAdvice(route.getRestBindingDefinition().createRestBindingAdvice(this)); + } catch (Exception e) { + throw ObjectHelper.wrapRuntimeCamelException(e); + } + } + // wrap in contract - if (contract != null) { + if (route.getInputType() != null || route.getOutputType() != null) { + Contract contract = new Contract(); + if (route.getInputType() != null) { + contract.setInputType(route.getInputType().getUrn()); + } + if (route.getOutputType() != null) { + contract.setOutputType(route.getOutputType().getUrn()); + } internal.addAdvice(new ContractAdvice(contract)); } @@ -409,7 +425,4 @@ public List getRoutePolicyList() { return routePolicyList; } - public void setContract(Contract contract) { - this.contract = contract; - } } diff --git a/camel-core/src/main/java/org/apache/camel/model/RouteDefinition.java b/camel-core/src/main/java/org/apache/camel/model/RouteDefinition.java index 76b4ec39a016f..97078dd552e44 100644 --- a/camel-core/src/main/java/org/apache/camel/model/RouteDefinition.java +++ b/camel-core/src/main/java/org/apache/camel/model/RouteDefinition.java @@ -44,6 +44,7 @@ import org.apache.camel.builder.ErrorHandlerBuilderRef; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.impl.DefaultRouteContext; +import org.apache.camel.model.rest.RestBindingDefinition; import org.apache.camel.model.rest.RestDefinition; import org.apache.camel.processor.interceptor.HandleFault; import org.apache.camel.spi.Contract; @@ -88,6 +89,7 @@ public class RouteDefinition extends ProcessorDefinition { private boolean contextScopedErrorHandler = true; private Boolean rest; private RestDefinition restDefinition; + private RestBindingDefinition restBindingDefinition; private InputTypeDefinition inputType; private OutputTypeDefinition outputType; @@ -966,6 +968,15 @@ public void setRestDefinition(RestDefinition restDefinition) { this.restDefinition = restDefinition; } + public RestBindingDefinition getRestBindingDefinition() { + return restBindingDefinition; + } + + @XmlTransient + public void setRestBindingDefinition(RestBindingDefinition restBindingDefinition) { + this.restBindingDefinition = restBindingDefinition; + } + @SuppressWarnings("deprecation") public boolean isContextScopedErrorHandler(CamelContext context) { if (!contextScopedErrorHandler) { @@ -1127,18 +1138,6 @@ protected RouteContext addRoutes(CamelContext camelContext, Collection ro throw new FailedToCreateRouteException(route.getId(), route.toString(), at, cause); } - // add data type contract - if (inputType != null || outputType != null) { - Contract contract = new Contract(); - if (inputType != null) { - contract.setInputType(inputType.getUrn()); - } - if (outputType != null) { - contract.setOutputType(outputType.getUrn()); - } - routeContext.setContract(contract); - } - List> list = new ArrayList>(outputs); for (ProcessorDefinition output : list) { try { 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 a3764425e0a17..8c1c291108129 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 @@ -27,8 +27,8 @@ import org.apache.camel.CamelContext; import org.apache.camel.Processor; -import org.apache.camel.component.rest.RestConsumerBindingProcessor; -import org.apache.camel.model.NoOutputDefinition; +import org.apache.camel.model.OptionalIdentifiedDefinition; +import org.apache.camel.processor.RestBindingAdvice; import org.apache.camel.spi.DataFormat; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.RestConfiguration; @@ -42,7 +42,7 @@ @Metadata(label = "rest") @XmlRootElement(name = "restBinding") @XmlAccessorType(XmlAccessType.FIELD) -public class RestBindingDefinition extends NoOutputDefinition { +public class RestBindingDefinition extends OptionalIdentifiedDefinition { @XmlTransient private Map defaultValues; @@ -80,8 +80,7 @@ public String toString() { return "RestBinding"; } - @Override - public Processor createProcessor(RouteContext routeContext) throws Exception { + public RestBindingAdvice createRestBindingAdvice(RouteContext routeContext) throws Exception { CamelContext context = routeContext.getCamelContext(); RestConfiguration config = context.getRestConfiguration(component, true); @@ -105,7 +104,7 @@ public Processor createProcessor(RouteContext routeContext) throws Exception { if (mode == null || "off".equals(mode)) { // binding mode is off, so create a off mode binding processor - return new RestConsumerBindingProcessor(context, null, null, null, null, consumes, produces, mode, skip, cors, corsHeaders, defaultValues); + return new RestBindingAdvice(context, null, null, null, null, consumes, produces, mode, skip, cors, corsHeaders, defaultValues); } // setup json data format @@ -200,7 +199,7 @@ public Processor createProcessor(RouteContext routeContext) throws Exception { setAdditionalConfiguration(config, context, outJaxb, "xml.out."); } - return new RestConsumerBindingProcessor(context, json, jaxb, outJson, outJaxb, consumes, produces, mode, skip, cors, corsHeaders, defaultValues); + return new RestBindingAdvice(context, json, jaxb, outJson, outJaxb, consumes, produces, mode, skip, cors, corsHeaders, defaultValues); } private void setAdditionalConfiguration(RestConfiguration config, CamelContext context, @@ -351,4 +350,9 @@ public Boolean getEnableCORS() { public void setEnableCORS(Boolean enableCORS) { this.enableCORS = enableCORS; } + + @Override + public String getLabel() { + return ""; + } } diff --git a/camel-core/src/main/java/org/apache/camel/model/rest/RestDefinition.java b/camel-core/src/main/java/org/apache/camel/model/rest/RestDefinition.java index 68deb669fea62..3cd6b2c1b8c46 100644 --- a/camel-core/src/main/java/org/apache/camel/model/rest/RestDefinition.java +++ b/camel-core/src/main/java/org/apache/camel/model/rest/RestDefinition.java @@ -736,7 +736,7 @@ private void addRouteDefinition(CamelContext camelContext, List } } - route.getOutputs().add(0, binding); + route.setRestBindingDefinition(binding); // create the from endpoint uri which is using the rest component String from = "rest:" + verb.asVerb() + ":" + buildUri(verb); diff --git a/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java b/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java index 15a3d61db9099..a3dad336c1cb5 100644 --- a/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java +++ b/camel-core/src/main/java/org/apache/camel/processor/ContractAdvice.java @@ -1,3 +1,19 @@ +/** + * 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; import org.apache.camel.CamelContext; @@ -40,12 +56,15 @@ public Object before(Exchange exchange) throws Exception { @Override public void after(Exchange exchange, Object data) throws Exception { Message target = exchange.hasOut() ? exchange.getOut() : exchange.getIn(); - DataType from = getCurrentType(exchange, exchange.hasOut() ? Exchange.OUTPUT_TYPE : Exchange.INPUT_TYPE); + if (!exchange.hasOut() && exchange.getProperty(Exchange.OUTPUT_TYPE) == null) { + exchange.setProperty(Exchange.OUTPUT_TYPE, exchange.getProperty(Exchange.INPUT_TYPE)); + } + DataType from = getCurrentType(exchange, Exchange.OUTPUT_TYPE); DataType to = contract.getOutputType(); if (to != null && !to.equals(from)) { LOG.debug("Looking for transformer for OUTPUT: from='{}', to='{}'", from, to); doTransform(target, from, to); - exchange.setProperty(exchange.hasOut() ? Exchange.OUTPUT_TYPE : Exchange.INPUT_TYPE, to); + exchange.setProperty(Exchange.OUTPUT_TYPE, to); } } @@ -61,7 +80,8 @@ private void doTransform(Message message, DataType from, DataType to) throws Exc // Java->Java transformation just relies on TypeConverter if no explicit transformer return; } else if (from == null) { - // {undefined}->Other transformation - assuming it's already in expected shape + // use body class as a from type, or do nothing with assuming it's already in expected shape + applyTransformerByClass(message, to); return; } else if (applyTransformerByToModel(message, from, to)) { // Java->Other transformation - found a transformer supports 'to' data model @@ -85,8 +105,8 @@ private void doTransform(Message message, DataType from, DataType to) throws Exc private boolean convertIfRequired(Message message, DataType type) throws Exception { // TODO for better performance it may be better to add TypeConveterTransformer // into transformer registry automatically to avoid unnecessary scan in transformer registry - CamelContext context = message.getExchange().getContext(); if (type != null && type.isJavaType() && type.getName() != null) { + CamelContext context = message.getExchange().getContext(); Class typeJava = getClazz(type.getName(), context); if (!typeJava.isAssignableFrom(message.getBody().getClass())) { LOG.debug("Converting to '{}'", typeJava.getName()); @@ -110,6 +130,12 @@ private boolean applyExactlyMatchedTransformer(Message message, DataType from, D return applyTransformer(transformer, message, from, to); } + private boolean applyTransformerByClass(Message message, DataType to) throws Exception { + DataType from = new DataType(message.getBody().getClass()); + Transformer transformer = message.getExchange().getContext().resolveTransformer(from, to); + return applyTransformer(transformer, message, from, to); + } + private boolean applyTransformerByToModel(Message message, DataType from, DataType to) throws Exception { Transformer transformer = message.getExchange().getContext().resolveTransformer(to.getModel()); return applyTransformer(transformer, message, from, to); diff --git a/camel-core/src/main/java/org/apache/camel/processor/RestBindingAdvice.java b/camel-core/src/main/java/org/apache/camel/processor/RestBindingAdvice.java new file mode 100644 index 0000000000000..7b81a1903ebc2 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/processor/RestBindingAdvice.java @@ -0,0 +1,418 @@ +/** + * 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; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +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.processor.binding.BindingException; +import org.apache.camel.spi.Contract; +import org.apache.camel.spi.DataFormat; +import org.apache.camel.spi.DataType; +import org.apache.camel.spi.RestConfiguration; +import org.apache.camel.spi.Transformer; +import org.apache.camel.util.ExchangeHelper; +import org.apache.camel.util.MessageHelper; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.ServiceHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link org.apache.camel.processor.CamelInternalProcessorAdvice} that binds the REST DSL incoming + * and outgoing messages from sources of json or xml to Java Objects. + *

+ * The binding uses {@link org.apache.camel.spi.DataFormat} for the actual work to transform + * from xml/json to Java Objects and reverse again. + * + * @see CamelInternalProcessor, CamelInternalProcessorAdvice + */ +public class RestBindingAdvice implements CamelInternalProcessorAdvice> { + private static final String STATE_KEY_DO_MARSHAL = "doMarshal"; + private static final String STATE_KEY_ACCEPT = "accept"; + private static final String STATE_JSON = "json"; + private static final String STATE_XML = "xml"; + + 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 corsHeaders; + private final Map queryDefaultValues; + + public RestBindingAdvice(CamelContext camelContext, DataFormat jsonDataFormat, DataFormat xmlDataFormat, + DataFormat outJsonDataFormat, DataFormat outXmlDataFormat, + String consumes, String produces, String bindingMode, + boolean skipBindingOnErrorCode, boolean enableCORS, + Map corsHeaders, + Map queryDefaultValues) throws Exception { + + 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; + } + + if (jsonMarshal != null) { + camelContext.addService(jsonMarshal, true); + } + if (jsonUnmarshal != null) { + camelContext.addService(jsonUnmarshal, true); + } + if (xmlMarshal instanceof CamelContextAware) { + camelContext.addService(xmlMarshal, true); + } + if (xmlUnmarshal instanceof CamelContextAware) { + camelContext.addService(xmlUnmarshal, true); + } + + this.consumes = consumes; + this.produces = produces; + this.bindingMode = bindingMode; + this.skipBindingOnErrorCode = skipBindingOnErrorCode; + this.enableCORS = enableCORS; + this.corsHeaders = corsHeaders; + this.queryDefaultValues = queryDefaultValues; + + } + + @Override + public Map before(Exchange exchange) throws Exception { + Map state = new HashMap<>(); + if (isOptionsMethod(exchange, state)) { + return state; + } + unmarshal(exchange, state); + return state; + } + + @Override + public void after(Exchange exchange, Map state) throws Exception { + if (enableCORS) { + setCORSHeaders(exchange, state); + } + if (state.get(STATE_KEY_DO_MARSHAL) != null) { + marshal(exchange, state); + } + } + + private boolean isOptionsMethod(Exchange exchange, Map state) { + 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); + return true; + } + return false; + } + + private void unmarshal(Exchange exchange, Map state) throws Exception { + 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"); + } + + // set the INPUT_TYPE to indicate body type + if (isJson) { + exchange.setProperty(Exchange.INPUT_TYPE, new DataType("json")); + } else if (isXml) { + exchange.setProperty(Exchange.INPUT_TYPE, new DataType("xml")); + } + + // 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"); + } + + state.put(STATE_KEY_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 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 + state.put(STATE_KEY_DO_MARSHAL, STATE_JSON); + if (ObjectHelper.isNotEmpty(body)) { + jsonUnmarshal.process(exchange); + ExchangeHelper.prepareOutToIn(exchange); + exchange.setProperty(Exchange.INPUT_TYPE, new DataType(exchange.getIn().getBody().getClass())); + } + return; + } else if (isXml && xmlUnmarshal != null) { + // add reverse operation + state.put(STATE_KEY_DO_MARSHAL, STATE_XML); + if (ObjectHelper.isNotEmpty(body)) { + xmlUnmarshal.process(exchange); + ExchangeHelper.prepareOutToIn(exchange); + exchange.setProperty(Exchange.INPUT_TYPE, new DataType(exchange.getIn().getBody().getClass())); + } + return; + } + + // we could not bind + if ("off".equals(bindingMode) || bindingMode.equals("auto")) { + // okay for auto we do not mind if we could not bind + state.put(STATE_KEY_DO_MARSHAL, STATE_JSON); + } 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)); + } + } + + } + + private void marshal(Exchange exchange, Map state) { + // 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 + String accept = (String)state.get(STATE_KEY_ACCEPT); + 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 = state.get(STATE_KEY_DO_MARSHAL).equals(STATE_XML); + isJson = !isXml; + } + + // 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); + exchange.setProperty(Exchange.OUTPUT_TYPE, new DataType("json")); + } + } else if (isXml && xmlMarshal != null) { + // only marshal if its xml content type + if (contentType.contains("xml")) { + xmlMarshal.process(exchange); + exchange.setProperty(Exchange.OUTPUT_TYPE, new DataType("xml")); + } + } 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"); + } + } + } + + private void setCORSHeaders(Exchange exchange, Map state) { + // 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); + } + +} \ No newline at end of file diff --git a/camel-core/src/main/java/org/apache/camel/spi/DataType.java b/camel-core/src/main/java/org/apache/camel/spi/DataType.java index 5d968399e00a5..973e5c4b6f48d 100644 --- a/camel-core/src/main/java/org/apache/camel/spi/DataType.java +++ b/camel-core/src/main/java/org/apache/camel/spi/DataType.java @@ -62,7 +62,7 @@ public boolean isJavaType() { @Override public String toString() { if (this.typeString == null) { - this.typeString = model + ":" + name; + this.typeString = name != null && !name.isEmpty() ? model + ":" + name : model; } return this.typeString; } diff --git a/camel-core/src/main/java/org/apache/camel/spi/RouteContext.java b/camel-core/src/main/java/org/apache/camel/spi/RouteContext.java index ce5711210efbf..54e6ad0c295a0 100644 --- a/camel-core/src/main/java/org/apache/camel/spi/RouteContext.java +++ b/camel-core/src/main/java/org/apache/camel/spi/RouteContext.java @@ -192,9 +192,4 @@ public interface RouteContext extends RuntimeConfiguration, EndpointAware { */ int getAndIncrement(ProcessorDefinition node); - /** - * Sets a {@link Contract} which declares input/output message type on the route. - * @param contract {@link Contract} for this route - */ - void setContract(Contract contract); } diff --git a/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEmbeddedRouteTest.java b/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEmbeddedRouteTest.java index df9fe91b36603..353dbcc7b9ea9 100644 --- a/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEmbeddedRouteTest.java +++ b/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEmbeddedRouteTest.java @@ -43,7 +43,7 @@ public void testFromRestModel() throws Exception { assertNotNull(rest); assertEquals("/say/hello", rest.getPath()); assertEquals(1, rest.getVerbs().size()); - ToDefinition to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(1)); + ToDefinition to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(0)); assertEquals("mock:hello", to.getUri()); rest = context.getRestDefinitions().get(1); @@ -51,7 +51,7 @@ public void testFromRestModel() throws Exception { assertEquals("/say/bye", rest.getPath()); assertEquals(2, rest.getVerbs().size()); assertEquals("application/json", rest.getVerbs().get(0).getConsumes()); - to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(1)); + to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(0)); assertEquals("mock:bye", to.getUri()); // the rest becomes routes and the input is a seda endpoint created by the DummyRestConsumerFactory diff --git a/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEndPathTest.java b/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEndPathTest.java index 14f8b8da15eff..7b899641958bc 100644 --- a/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEndPathTest.java +++ b/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetEndPathTest.java @@ -38,7 +38,7 @@ public void testFromRestModel() throws Exception { assertEquals("/say/bye", rest.getPath()); assertEquals(2, rest.getVerbs().size()); assertEquals("application/json", rest.getVerbs().get(0).getConsumes()); - to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(1)); + to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(0)); assertEquals("direct:bye", to.getUri()); // the rest becomes routes and the input is a seda endpoint created by the DummyRestConsumerFactory diff --git a/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetInterceptTest.java b/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetInterceptTest.java index 62ed09cf4b2c3..6df660de04315 100644 --- a/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetInterceptTest.java +++ b/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetInterceptTest.java @@ -32,7 +32,7 @@ protected JndiRegistry createRegistry() throws Exception { public void testFromRestModel() throws Exception { getMockEndpoint("mock:hello").expectedMessageCount(1); getMockEndpoint("mock:bar").expectedMessageCount(1); - getMockEndpoint("mock:intercept").expectedMessageCount(4); + getMockEndpoint("mock:intercept").expectedMessageCount(3); String out = template.requestBody("seda:get-say-hello", "I was here", String.class); assertEquals("Bye World", out); diff --git a/camel-core/src/test/java/org/apache/camel/management/ManagedTransformerRegistryTest.java b/camel-core/src/test/java/org/apache/camel/management/ManagedTransformerRegistryTest.java index f79b49286f90a..96918a6d88762 100644 --- a/camel-core/src/test/java/org/apache/camel/management/ManagedTransformerRegistryTest.java +++ b/camel-core/src/test/java/org/apache/camel/management/ManagedTransformerRegistryTest.java @@ -107,8 +107,8 @@ public void testManageTransformerRegistry() throws Exception { assertEquals("xml:test", to); } else if (description.startsWith("MyTransformer")) { assertEquals("custom", scheme); - assertEquals("null:null", from); - assertEquals("null:null", to); + assertEquals(null, from); + assertEquals(null, to); } else { fail("Unexpected transformer:" + description); } diff --git a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeJsonWithContractTest.java b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeJsonWithContractTest.java new file mode 100644 index 0000000000000..d72dab1fbead5 --- /dev/null +++ b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeJsonWithContractTest.java @@ -0,0 +1,92 @@ +/** + * 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.jetty.rest; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.jetty.BaseJettyTest; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestJettyBindingModeJsonWithContractTest extends BaseJettyTest { + + @Test + public void testBindingModeJsonWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBody("http://localhost:" + getPort() + "/users/new", body); + assertNotNull(answer); + BufferedReader reader = new BufferedReader(new InputStreamReader((InputStream)answer)); + String line; + String answerString = ""; + while ((line = reader.readLine()) != null) { + answerString += line; + } + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + context.getTypeConverterRegistry().addTypeConverters(new MyTypeConverters()); + restConfiguration().component("jetty").host("localhost").port(getPort()).bindingMode(RestBindingMode.json); + + rest("/users/") + // REST binding converts from JSON to UserPojo + .post("new").type(UserPojo.class) + .route() + // then contract advice converts from UserPojo to UserPojoEx + .inputType(UserPojoEx.class) + .to("mock:input"); + } + }; + } + + public static class MyTypeConverters implements TypeConverters { + @Converter + public UserPojoEx toEx(UserPojo user) { + UserPojoEx ex = new UserPojoEx(); + ex.setId(user.getId()); + ex.setName(user.getName()); + ex.setActive(true); + return ex; + } + } +} diff --git a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeOffWithContractTest.java b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeOffWithContractTest.java new file mode 100644 index 0000000000000..fa1725707e925 --- /dev/null +++ b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/RestJettyBindingModeOffWithContractTest.java @@ -0,0 +1,99 @@ +/** + * 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.jetty.rest; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.jetty.BaseJettyTest; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.model.dataformat.JsonDataFormat; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestJettyBindingModeOffWithContractTest extends BaseJettyTest { + + @Test + public void testBindingModeOffWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBodyAndHeader("http://localhost:" + getPort() + "/users/new", body, Exchange.CONTENT_TYPE, "application/json"); + assertNotNull(answer); + BufferedReader reader = new BufferedReader(new InputStreamReader((InputStream)answer)); + String line; + String answerString = ""; + while ((line = reader.readLine()) != null) { + answerString += line; + } + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().component("jetty").host("localhost").port(getPort()).bindingMode(RestBindingMode.off); + + JsonDataFormat jsondf = new JsonDataFormat(); + jsondf.setLibrary(JsonLibrary.Jackson); + jsondf.setAllowUnmarshallType(true); + jsondf.setUnmarshalType(UserPojoEx.class); + transformer() + .fromType("json") + .toType(UserPojoEx.class) + .withDataFormat(jsondf); + transformer() + .fromType(UserPojoEx.class) + .toType("json") + .withDataFormat(jsondf); + rest("/users/") + // REST binding does nothing + .post("new") + .route() + // contract advice converts betweeen JSON and UserPojoEx directly + .inputType(UserPojoEx.class) + .outputType("json") + .process(ex -> { + ex.getIn().getBody(UserPojoEx.class).setActive(true); + }) + .to("mock:input"); + } + }; + } + +} diff --git a/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/UserPojoEx.java b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/UserPojoEx.java new file mode 100644 index 0000000000000..5ae627ab94286 --- /dev/null +++ b/components/camel-jetty9/src/test/java/org/apache/camel/component/jetty/rest/UserPojoEx.java @@ -0,0 +1,48 @@ +/** + * 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.jetty.rest; + +public class UserPojoEx { + + private int id; + private String name; + private boolean active; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeJsonWithContractTest.java b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeJsonWithContractTest.java new file mode 100644 index 0000000000000..d522064089bda --- /dev/null +++ b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeJsonWithContractTest.java @@ -0,0 +1,83 @@ +/** + * 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.netty4.http.rest; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.netty4.http.BaseNettyTest; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestNettyHttpBindingModeJsonWithContractTest extends BaseNettyTest { + + @Test + public void testBindingModeJsonWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBody("netty4-http:http://localhost:" + getPort() + "/users/new", body); + assertNotNull(answer); + String answerString = new String((byte[])answer); + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + context.getTypeConverterRegistry().addTypeConverters(new MyTypeConverters()); + restConfiguration().component("netty4-http").host("localhost").port(getPort()).bindingMode(RestBindingMode.json); + + rest("/users/") + // REST binding converts from JSON to UserPojo + .post("new").type(UserPojo.class) + .route() + // then contract advice converts from UserPojo to UserPojoEx + .inputType(UserPojoEx.class) + .to("mock:input"); + } + }; + } + + public static class MyTypeConverters implements TypeConverters { + @Converter + public UserPojoEx toEx(UserPojo user) { + UserPojoEx ex = new UserPojoEx(); + ex.setId(user.getId()); + ex.setName(user.getName()); + ex.setActive(true); + return ex; + } + } +} diff --git a/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeOffWithContractTest.java b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeOffWithContractTest.java new file mode 100644 index 0000000000000..81ca1ad3c66d9 --- /dev/null +++ b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/RestNettyHttpBindingModeOffWithContractTest.java @@ -0,0 +1,90 @@ +/** + * 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.netty4.http.rest; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.netty4.http.BaseNettyTest; +import org.apache.camel.model.dataformat.JsonDataFormat; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestNettyHttpBindingModeOffWithContractTest extends BaseNettyTest { + + @Test + public void testBindingModeOffWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBodyAndHeader("netty4-http:http://localhost:" + getPort() + "/users/new", body, Exchange.CONTENT_TYPE, "application/json"); + assertNotNull(answer); + String answerString = new String((byte[])answer); + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().component("netty4-http").host("localhost").port(getPort()).bindingMode(RestBindingMode.off); + + JsonDataFormat jsondf = new JsonDataFormat(); + jsondf.setLibrary(JsonLibrary.Jackson); + jsondf.setAllowUnmarshallType(true); + jsondf.setUnmarshalType(UserPojoEx.class); + transformer() + .fromType("json") + .toType(UserPojoEx.class) + .withDataFormat(jsondf); + transformer() + .fromType(UserPojoEx.class) + .toType("json") + .withDataFormat(jsondf); + rest("/users/") + // REST binding does nothing + .post("new") + .route() + // contract advice converts betweeen JSON and UserPojoEx directly + .inputType(UserPojoEx.class) + .outputType("json") + .process(ex -> { + ex.getIn().getBody(UserPojoEx.class).setActive(true); + }) + .to("mock:input"); + } + }; + } + +} diff --git a/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/UserPojoEx.java b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/UserPojoEx.java new file mode 100644 index 0000000000000..48d246641afae --- /dev/null +++ b/components/camel-netty4-http/src/test/java/org/apache/camel/component/netty4/http/rest/UserPojoEx.java @@ -0,0 +1,48 @@ +/** + * 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.netty4.http.rest; + +public class UserPojoEx { + + private int id; + private String name; + private boolean active; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeJsonWithContractTest.java b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeJsonWithContractTest.java new file mode 100644 index 0000000000000..b25aabdffa866 --- /dev/null +++ b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeJsonWithContractTest.java @@ -0,0 +1,91 @@ +/** + * 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.restlet; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestRestletBindingModeJsonWithContractTest extends RestletTestSupport { + + @Test + public void testBindingModeJsonWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBody("http://localhost:" + portNum + "/users/new", body); + assertNotNull(answer); + BufferedReader reader = new BufferedReader(new InputStreamReader((InputStream)answer)); + String line; + String answerString = ""; + while ((line = reader.readLine()) != null) { + answerString += line; + } + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + context.getTypeConverterRegistry().addTypeConverters(new MyTypeConverters()); + restConfiguration().component("restlet").host("localhost").port(portNum).bindingMode(RestBindingMode.json); + + rest("/users/") + // REST binding converts from JSON to UserPojo + .post("new").type(UserPojo.class) + .route() + // then contract advice converts from UserPojo to UserPojoEx + .inputType(UserPojoEx.class) + .to("mock:input"); + } + }; + } + + public static class MyTypeConverters implements TypeConverters { + @Converter + public UserPojoEx toEx(UserPojo user) { + UserPojoEx ex = new UserPojoEx(); + ex.setId(user.getId()); + ex.setName(user.getName()); + ex.setActive(true); + return ex; + } + } +} diff --git a/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeOffWithContractTest.java b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeOffWithContractTest.java new file mode 100644 index 0000000000000..98bae987e8ac1 --- /dev/null +++ b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletBindingModeOffWithContractTest.java @@ -0,0 +1,98 @@ +/** + * 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.restlet; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.model.dataformat.JsonDataFormat; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestRestletBindingModeOffWithContractTest extends RestletTestSupport { + + @Test + public void testBindingModeOffWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBodyAndHeader("http://localhost:" + portNum + "/users/new", body, Exchange.CONTENT_TYPE, "application/json"); + assertNotNull(answer); + BufferedReader reader = new BufferedReader(new InputStreamReader((InputStream)answer)); + String line; + String answerString = ""; + while ((line = reader.readLine()) != null) { + answerString += line; + } + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().component("restlet").host("localhost").port(portNum).bindingMode(RestBindingMode.off); + + JsonDataFormat jsondf = new JsonDataFormat(); + jsondf.setLibrary(JsonLibrary.Jackson); + jsondf.setAllowUnmarshallType(true); + jsondf.setUnmarshalType(UserPojoEx.class); + transformer() + .fromType("json") + .toType(UserPojoEx.class) + .withDataFormat(jsondf); + transformer() + .fromType(UserPojoEx.class) + .toType("json") + .withDataFormat(jsondf); + rest("/users/") + // REST binding does nothing + .post("new") + .route() + // contract advice converts betweeen JSON and UserPojoEx directly + .inputType(UserPojoEx.class) + .outputType("json") + .process(ex -> { + ex.getIn().getBody(UserPojoEx.class).setActive(true); + }) + .to("mock:input"); + } + }; + } + +} diff --git a/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletCustomDataFormatInvalidTest.java b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletCustomDataFormatInvalidTest.java index e75c2910acc5b..751ea9c6f2773 100644 --- a/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletCustomDataFormatInvalidTest.java +++ b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/RestRestletCustomDataFormatInvalidTest.java @@ -62,7 +62,7 @@ public void configure() throws Exception { context.start(); fail("Should have thrown exception"); } catch (FailedToCreateRouteException e) { - assertEquals("JsonDataFormat name: bla must not be an existing bean instance from the registry", e.getCause().getMessage()); + assertTrue(e.getCause().getMessage().contains("JsonDataFormat name: bla must not be an existing bean instance from the registry")); } } diff --git a/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/UserPojoEx.java b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/UserPojoEx.java new file mode 100644 index 0000000000000..eabc2aa8c8194 --- /dev/null +++ b/components/camel-restlet/src/test/java/org/apache/camel/component/restlet/UserPojoEx.java @@ -0,0 +1,48 @@ +/** + * 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.restlet; + +public class UserPojoEx { + + private int id; + private String name; + private boolean active; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeJsonWithContractTest.java b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeJsonWithContractTest.java new file mode 100644 index 0000000000000..8e91d66f69ab4 --- /dev/null +++ b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeJsonWithContractTest.java @@ -0,0 +1,92 @@ +/** + * 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.servlet.rest; + +import java.io.ByteArrayInputStream; + +import com.meterware.httpunit.PostMethodWebRequest; +import com.meterware.httpunit.WebRequest; +import com.meterware.httpunit.WebResponse; +import com.meterware.servletunit.ServletUnitClient; +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.servlet.ServletCamelRouterTestSupport; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestServletBindingModeJsonWithContractTest extends ServletCamelRouterTestSupport { + + @Test + public void testBindingModeJsonWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + WebRequest req = new PostMethodWebRequest(CONTEXT_URL + "/services/users/new", new ByteArrayInputStream(body.getBytes()), "application/json"); + ServletUnitClient client = newClient(); + client.setExceptionsThrownOnErrorStatus(false); + WebResponse response = client.getResponse(req); + assertEquals(200, response.getResponseCode()); + String answer = response.getText(); + assertTrue("Unexpected response: " + answer, answer.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + context.getTypeConverterRegistry().addTypeConverters(new MyTypeConverters()); + restConfiguration().component("servlet").bindingMode(RestBindingMode.json); + + rest("/users/") + // REST binding converts from JSON to UserPojo + .post("new").type(UserPojo.class) + .route() + // then contract advice converts from UserPojo to UserPojoEx + .inputType(UserPojoEx.class) + .to("mock:input"); + } + }; + } + + public static class MyTypeConverters implements TypeConverters { + @Converter + public UserPojoEx toEx(UserPojo user) { + UserPojoEx ex = new UserPojoEx(); + ex.setId(user.getId()); + ex.setName(user.getName()); + ex.setActive(true); + return ex; + } + } +} diff --git a/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeOffWithContractTest.java b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeOffWithContractTest.java new file mode 100644 index 0000000000000..fa481be551619 --- /dev/null +++ b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/RestServletBindingModeOffWithContractTest.java @@ -0,0 +1,99 @@ +/** + * 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.servlet.rest; + +import java.io.ByteArrayInputStream; + +import com.meterware.httpunit.PostMethodWebRequest; +import com.meterware.httpunit.WebRequest; +import com.meterware.httpunit.WebResponse; +import com.meterware.servletunit.ServletUnitClient; +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.servlet.ServletCamelRouterTestSupport; +import org.apache.camel.model.dataformat.JsonDataFormat; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestServletBindingModeOffWithContractTest extends ServletCamelRouterTestSupport { + + @Test + public void testBindingModeOffWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + WebRequest req = new PostMethodWebRequest(CONTEXT_URL + "/services/users/new", new ByteArrayInputStream(body.getBytes()), "application/json"); + ServletUnitClient client = newClient(); + client.setExceptionsThrownOnErrorStatus(false); + WebResponse response = client.getResponse(req); + assertEquals(200, response.getResponseCode()); + String answer = response.getText(); + assertTrue("Unexpected response: " + answer, answer.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().component("servlet").bindingMode(RestBindingMode.off); + + JsonDataFormat jsondf = new JsonDataFormat(); + jsondf.setLibrary(JsonLibrary.Jackson); + jsondf.setAllowUnmarshallType(true); + jsondf.setUnmarshalType(UserPojoEx.class); + transformer() + .fromType("json") + .toType(UserPojoEx.class) + .withDataFormat(jsondf); + transformer() + .fromType(UserPojoEx.class) + .toType("json") + .withDataFormat(jsondf); + rest("/users/") + // REST binding does nothing + .post("new") + .route() + // contract advice converts betweeen JSON and UserPojoEx directly + .inputType(UserPojoEx.class) + .outputType("json") + .process(ex -> { + ex.getIn().getBody(UserPojoEx.class).setActive(true); + }) + .to("mock:input"); + } + }; + } + +} diff --git a/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/UserPojoEx.java b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/UserPojoEx.java new file mode 100644 index 0000000000000..7b1b218b0b5df --- /dev/null +++ b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/rest/UserPojoEx.java @@ -0,0 +1,48 @@ +/** + * 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.servlet.rest; + +public class UserPojoEx { + + private int id; + private String name; + private boolean active; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +} diff --git a/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetEmbeddedRouteTest.java b/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetEmbeddedRouteTest.java index c9cafe8f8c5ba..ed9a579353890 100644 --- a/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetEmbeddedRouteTest.java +++ b/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetEmbeddedRouteTest.java @@ -41,7 +41,7 @@ public void testFromRestModel() throws Exception { assertNotNull(rest); assertEquals("/say/hello", rest.getPath()); assertEquals(1, rest.getVerbs().size()); - ToDefinition to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(1)); + ToDefinition to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(0)); assertEquals("mock:hello", to.getUri()); rest = context.getRestDefinitions().get(1); @@ -49,7 +49,7 @@ public void testFromRestModel() throws Exception { assertEquals("/say/bye", rest.getPath()); assertEquals(2, rest.getVerbs().size()); assertEquals("application/json", rest.getVerbs().get(0).getConsumes()); - to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(1)); + to = assertIsInstanceOf(ToDefinition.class, rest.getVerbs().get(0).getRoute().getOutputs().get(0)); assertEquals("mock:bye", to.getUri()); // the rest becomes routes and the input is a seda endpoint created by the DummyRestConsumerFactory diff --git a/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetInterceptTest.java b/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetInterceptTest.java index ce766b4e66c93..6b7236832717a 100644 --- a/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetInterceptTest.java +++ b/components/camel-test-blueprint/src/test/java/org/apache/camel/test/blueprint/component/rest/FromRestGetInterceptTest.java @@ -30,7 +30,7 @@ protected String getBlueprintDescriptor() { public void testFromRestModel() throws Exception { getMockEndpoint("mock:hello").expectedMessageCount(1); getMockEndpoint("mock:bar").expectedMessageCount(1); - getMockEndpoint("mock:intercept").expectedMessageCount(4); + getMockEndpoint("mock:intercept").expectedMessageCount(3); String out = template.requestBody("seda:get-say-hello", "I was here", String.class); assertEquals("Bye World", out); diff --git a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeJsonWithContractTest.java b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeJsonWithContractTest.java new file mode 100644 index 0000000000000..df293dddf951f --- /dev/null +++ b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeJsonWithContractTest.java @@ -0,0 +1,83 @@ +/** + * 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.undertow.rest; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.undertow.BaseUndertowTest; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestUndertowHttpBindingModeJsonWithContractTest extends BaseUndertowTest { + + @Test + public void testBindingModeJsonWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBody("undertow:http://localhost:{{port}}/users/new", body); + assertNotNull(answer); + String answerString = new String((byte[])answer); + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + context.getTypeConverterRegistry().addTypeConverters(new MyTypeConverters()); + restConfiguration().component("undertow").host("localhost").port(getPort()).bindingMode(RestBindingMode.json); + + rest("/users/") + // REST binding converts from JSON to UserPojo + .post("new").type(UserPojo.class) + .route() + // then contract advice converts from UserPojo to UserPojoEx + .inputType(UserPojoEx.class) + .to("mock:input"); + } + }; + } + + public static class MyTypeConverters implements TypeConverters { + @Converter + public UserPojoEx toEx(UserPojo user) { + UserPojoEx ex = new UserPojoEx(); + ex.setId(user.getId()); + ex.setName(user.getName()); + ex.setActive(true); + return ex; + } + } +} diff --git a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeOffWithContractTest.java b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeOffWithContractTest.java new file mode 100644 index 0000000000000..432c986ea8df7 --- /dev/null +++ b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/RestUndertowHttpBindingModeOffWithContractTest.java @@ -0,0 +1,90 @@ +/** + * 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.undertow.rest; + +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.TypeConverters; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.component.undertow.BaseUndertowTest; +import org.apache.camel.model.dataformat.JsonDataFormat; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.junit.Test; + +public class RestUndertowHttpBindingModeOffWithContractTest extends BaseUndertowTest { + + @Test + public void testBindingModeOffWithContract() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:input"); + mock.expectedMessageCount(1); + mock.message(0).body().isInstanceOf(UserPojoEx.class); + + String body = "{\"id\": 123, \"name\": \"Donald Duck\"}"; + Object answer = template.requestBodyAndHeader("undertow:http://localhost:{{port}}/users/new", body, Exchange.CONTENT_TYPE, "application/json"); + assertNotNull(answer); + String answerString = new String((byte[])answer); + assertTrue("Unexpected response: " + answerString, answerString.contains("\"active\":true")); + + assertMockEndpointsSatisfied(); + + Object obj = mock.getReceivedExchanges().get(0).getIn().getBody(); + assertEquals(UserPojoEx.class, obj.getClass()); + UserPojoEx user = (UserPojoEx)obj; + assertNotNull(user); + assertEquals(123, user.getId()); + assertEquals("Donald Duck", user.getName()); + assertEquals(true, user.isActive()); + } + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + restConfiguration().component("undertow").host("localhost").port(getPort()).bindingMode(RestBindingMode.off); + + JsonDataFormat jsondf = new JsonDataFormat(); + jsondf.setLibrary(JsonLibrary.Jackson); + jsondf.setAllowUnmarshallType(true); + jsondf.setUnmarshalType(UserPojoEx.class); + transformer() + .fromType("json") + .toType(UserPojoEx.class) + .withDataFormat(jsondf); + transformer() + .fromType(UserPojoEx.class) + .toType("json") + .withDataFormat(jsondf); + rest("/users/") + // REST binding does nothing + .post("new") + .route() + // contract advice converts betweeen JSON and UserPojoEx directly + .inputType(UserPojoEx.class) + .outputType("json") + .process(ex -> { + ex.getIn().getBody(UserPojoEx.class).setActive(true); + }) + .to("mock:input"); + } + }; + } + +} diff --git a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/UserPojoEx.java b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/UserPojoEx.java new file mode 100644 index 0000000000000..974778cf10921 --- /dev/null +++ b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/rest/UserPojoEx.java @@ -0,0 +1,48 @@ +/** + * 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.undertow.rest; + +public class UserPojoEx { + + private int id; + private String name; + private boolean active; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } +}