From 851cb6af1f2ba392d4810931df0719f0d1b80c0b Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Fri, 20 Aug 2021 14:50:07 +0200 Subject: [PATCH] chore: Improve Apache Camel support - Make sure to set CamelContext on fluent API - Add integration tests on data format, transform and process - Add convenient convertBodyTo method in CamelSupport API - Support endpoint DSL builder - Add route processor that performs route logic as part of the send/receive operation - Improve Camel test action fluent API - Update documentation --- .../citrus/endpoint/EndpointUriBuilder.java | 29 + endpoints/citrus-camel/pom.xml | 11 + .../camel/actions/CamelControlBusAction.java | 74 +- .../actions/CamelRouteActionBuilder.java | 42 +- .../camel/actions/CreateCamelRouteAction.java | 2 +- .../citrus/camel/dsl/CamelContextAware.java | 30 + .../consol/citrus/camel/dsl/CamelSupport.java | 94 +- .../dsl/EndpointBuilderFactorySupport.java | 28 + .../citrus/camel/endpoint/CamelConsumer.java | 17 +- .../camel/endpoint/CamelEndpointBuilder.java | 22 + .../endpoint/CamelEndpointConfiguration.java | 19 + .../citrus/camel/endpoint/CamelProducer.java | 26 +- .../camel/endpoint/CamelSyncConsumer.java | 17 +- .../endpoint/CamelSyncEndpointBuilder.java | 11 + .../camel/endpoint/CamelSyncProducer.java | 21 +- .../CamelDataFormatMessageProcessor.java | 4 + .../camel/message/CamelMessageProcessor.java | 4 +- .../camel/message/CamelRouteProcessor.java | 150 +++ .../CamelTransformMessageProcessor.java | 7 + .../format/DataFormatClauseSupport.java | 16 + .../actions/CreateCamelRouteActionTest.java | 4 +- .../camel/integration/CamelControlbusIT.java | 88 ++ .../camel/integration/CamelDataFormatIT.java | 61 ++ .../camel/integration/CamelProcessorIT.java | 73 ++ .../camel/integration/CamelRouteActionIT.java | 120 +++ .../integration/CamelRouteProcessorIT.java | 117 +++ .../camel/integration/CamelTransformIT.java | 77 ++ pom.xml | 5 + src/manual/endpoint-camel.adoc | 928 +++++++++++++++--- .../dsl/builder/CamelRouteActionBuilder.java | 13 +- 30 files changed, 1891 insertions(+), 219 deletions(-) create mode 100644 core/citrus-api/src/main/java/com/consol/citrus/endpoint/EndpointUriBuilder.java create mode 100644 endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelContextAware.java create mode 100644 endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/EndpointBuilderFactorySupport.java create mode 100644 endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelRouteProcessor.java create mode 100644 endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelControlbusIT.java create mode 100644 endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelDataFormatIT.java create mode 100644 endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelProcessorIT.java create mode 100644 endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteActionIT.java create mode 100644 endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteProcessorIT.java create mode 100644 endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelTransformIT.java diff --git a/core/citrus-api/src/main/java/com/consol/citrus/endpoint/EndpointUriBuilder.java b/core/citrus-api/src/main/java/com/consol/citrus/endpoint/EndpointUriBuilder.java new file mode 100644 index 0000000000..6473b37f78 --- /dev/null +++ b/core/citrus-api/src/main/java/com/consol/citrus/endpoint/EndpointUriBuilder.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 com.consol.citrus.endpoint; + +/** + * @author Christoph Deppisch + */ +@FunctionalInterface +public interface EndpointUriBuilder { + + String getUri(); +} diff --git a/endpoints/citrus-camel/pom.xml b/endpoints/citrus-camel/pom.xml index 4d1b765d6f..da2a0066a8 100644 --- a/endpoints/citrus-camel/pom.xml +++ b/endpoints/citrus-camel/pom.xml @@ -42,6 +42,11 @@ org.apache.camel camel-spring-xml + + org.apache.camel + camel-endpointdsl + provided + jakarta.xml.bind @@ -79,6 +84,12 @@ ${apache.camel.version} test + + org.apache.camel + camel-jsonpath + ${apache.camel.version} + test + diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CamelControlBusAction.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CamelControlBusAction.java index e699ddd952..0b58097c3f 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CamelControlBusAction.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CamelControlBusAction.java @@ -37,7 +37,7 @@ public class CamelControlBusAction extends AbstractCamelRouteAction { /** Logger */ - private static Logger log = LoggerFactory.getLogger(CamelControlBusAction.class); + private static final Logger LOG = LoggerFactory.getLogger(CamelControlBusAction.class); /** The control bus action */ private final String action; @@ -90,12 +90,12 @@ public void doExecute(TestContext context) { if (StringUtils.hasText(result)) { String expectedResult = context.replaceDynamicContentInString(result); - if (log.isDebugEnabled()) { - log.debug(String.format("Validating Camel controlbus response = '%s'", expectedResult)); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Validating Camel controlbus response = '%s'", expectedResult)); } ValidationUtils.validateValues(response.getPayload(String.class), expectedResult, "camelControlBusResult", context); - log.info("Validation of Camel controlbus response successful - All values OK"); + LOG.info("Validation of Camel controlbus response successful - All values OK"); } } @@ -150,6 +150,23 @@ public static final class Builder extends AbstractCamelRouteAction.Builder routeIds = new ArrayList<>(); private TestActionBuilder delegate; @@ -33,6 +29,15 @@ public static CamelRouteActionBuilder camel() { return new CamelRouteActionBuilder(); } + /** + * Processor calling given Camel route as part of the message processing. + * @return + */ + public CamelRouteProcessor.Builder processor() { + return CamelRouteProcessor.Builder.route() + .camelContext(camelContext); + } + /** * Sets the Camel context to use. * @param camelContext @@ -54,25 +59,6 @@ public CamelRouteActionBuilder context(CamelContext camelContext) { return this; } - /** - * Adds route ids. - * @param routeIds - * @return - */ - public CamelRouteActionBuilder routes(String... routeIds) { - return routes(Arrays.asList(routeIds)); - } - - /** - * Add list of route ids. - * @param routeIds - * @return - */ - public CamelRouteActionBuilder routes(List routeIds) { - this.routeIds.addAll(routeIds); - return this; - } - /** * Creates new Camel routes in route builder. * @param routeBuilder @@ -85,7 +71,6 @@ public CreateCamelRouteAction.Builder create(RouteBuilder routeBuilder) { CreateCamelRouteAction.Builder builder = new CreateCamelRouteAction.Builder() .context(getCamelContext()) - .routeIds(routeIds) .route(routeBuilder); this.delegate = builder; @@ -100,7 +85,6 @@ public CreateCamelRouteAction.Builder create(RouteBuilder routeBuilder) { public CreateCamelRouteAction.Builder create(String routeContext) { CreateCamelRouteAction.Builder builder = new CreateCamelRouteAction.Builder() .context(getCamelContext()) - .routeIds(routeIds) .routeContext(routeContext); this.delegate = builder; @@ -113,8 +97,7 @@ public CreateCamelRouteAction.Builder create(String routeContext) { */ public CamelControlBusAction.Builder controlBus() { CamelControlBusAction.Builder builder = new CamelControlBusAction.Builder() - .context(getCamelContext()) - .routeIds(routeIds); + .context(getCamelContext()); this.delegate = builder; return builder; @@ -126,7 +109,6 @@ public CamelControlBusAction.Builder controlBus() { public StartCamelRouteAction.Builder start(String ... routes) { StartCamelRouteAction.Builder builder = new StartCamelRouteAction.Builder() .context(getCamelContext()) - .routeIds(routeIds) .routes(routes); this.delegate = builder; @@ -139,7 +121,6 @@ public StartCamelRouteAction.Builder start(String ... routes) { public StopCamelRouteAction.Builder stop(String ... routes) { StopCamelRouteAction.Builder builder = new StopCamelRouteAction.Builder() .context(getCamelContext()) - .routeIds(routeIds) .routes(routes); this.delegate = builder; @@ -152,7 +133,6 @@ public StopCamelRouteAction.Builder stop(String ... routes) { public RemoveCamelRouteAction.Builder remove(String ... routes) { RemoveCamelRouteAction.Builder builder = new RemoveCamelRouteAction.Builder() .context(getCamelContext()) - .routeIds(routeIds) .routes(routes); this.delegate = builder; diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CreateCamelRouteAction.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CreateCamelRouteAction.java index dff8a62ede..677d2aefb0 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CreateCamelRouteAction.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/actions/CreateCamelRouteAction.java @@ -125,7 +125,7 @@ public String getRouteContext() { */ public static final class Builder extends AbstractCamelRouteAction.Builder { - private List routes = new ArrayList<>(); + private final List routes = new ArrayList<>(); private String routeContext; public Builder route(RouteBuilder routeBuilder) { diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelContextAware.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelContextAware.java new file mode 100644 index 0000000000..d9c7b7e2ad --- /dev/null +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelContextAware.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 com.consol.citrus.camel.dsl; + +import org.apache.camel.CamelContext; + +/** + * @author Christoph Deppisch + */ +public interface CamelContextAware { + + T camelContext(CamelContext camelContext); +} diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelSupport.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelSupport.java index 4a55d4a2c6..19badd1cec 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelSupport.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/CamelSupport.java @@ -19,12 +19,21 @@ package com.consol.citrus.camel.dsl; +import java.util.function.Function; + +import com.consol.citrus.camel.actions.CamelControlBusAction; +import com.consol.citrus.camel.actions.CamelRouteActionBuilder; import com.consol.citrus.camel.message.CamelDataFormatMessageProcessor; import com.consol.citrus.camel.message.CamelMessageProcessor; +import com.consol.citrus.camel.message.CamelRouteProcessor; import com.consol.citrus.camel.message.CamelTransformMessageProcessor; import com.consol.citrus.camel.message.format.DataFormatClauseSupport; +import com.consol.citrus.endpoint.EndpointUriBuilder; +import org.apache.camel.CamelContext; import org.apache.camel.Processor; import org.apache.camel.builder.ExpressionClauseSupport; +import org.apache.camel.model.OutputDefinition; +import org.apache.camel.model.ProcessorDefinition; /** * Support class combining all available Apache Camel Java DSL capabilities. @@ -32,6 +41,8 @@ */ public class CamelSupport { + private CamelContext camelContext; + /** * Static entrance for all Camel related Java DSL functionalities. * @return @@ -40,13 +51,80 @@ public static CamelSupport camel() { return new CamelSupport(); } + /** + * Static entrance for all Camel related Java DSL functionalities. + * @return + */ + public static CamelSupport camel(CamelContext camelContext) { + return new CamelSupport() + .camelContext(camelContext); + } + + /** + * Static entrance for all Camel related Java DSL functionalities. + * @return + */ + public CamelSupport camelContext(CamelContext camelContext) { + this.camelContext = camelContext; + return this; + } + + /** + * Constructs proper endpoint uri from endpoint uri builder. + * @return + */ + public String endpoint(EndpointUriBuilder builder) { + return "camel:" + builder.getUri(); + } + + /** + * Entry point for the Camel endpoint builder DSL. + * @return + */ + public EndpointBuilderFactorySupport endpoints() { + return new EndpointBuilderFactorySupport(); + } + + /** + * Creates new control bus tet action builder and sets the Camel context. + * @return + */ + public CamelControlBusAction.Builder controlBus() { + return CamelControlBusAction.Builder.controlBus() + .context(camelContext); + } + /** * Message processor delegating to given Apache Camel processor. * @param processor * @return */ public CamelMessageProcessor.Builder process(Processor processor) { - return CamelMessageProcessor.Builder.process(processor); + return CamelMessageProcessor.Builder.process(processor) + .camelContext(camelContext); + } + + /** + * Perform actions on a Camel route such as start/stop/create and process. + * @return + */ + public CamelRouteActionBuilder route() { + return new CamelRouteActionBuilder() + .context(camelContext); + } + + /** + * Route processor delegating to given Apache Camel processor. + * @return + */ + public CamelRouteProcessor.Builder route( + Function, ProcessorDefinition> configurer) { + CamelRouteProcessor.Builder builder = CamelRouteProcessor.Builder.route() + .camelContext(camelContext); + + configurer.apply(builder); + + return builder; } /** @@ -54,7 +132,15 @@ public CamelMessageProcessor.Builder process(Processor processor) { * @return */ public ExpressionClauseSupport transform() { - return CamelTransformMessageProcessor.Builder.transform(); + return CamelTransformMessageProcessor.Builder.transform(camelContext); + } + + /** + * Transform message and convert body to given type using the Camel message converter implementation. + * @return + */ + public CamelTransformMessageProcessor.Builder convertBodyTo(Class type) { + return CamelTransformMessageProcessor.Builder.transform(camelContext).body(type); } /** @@ -62,7 +148,7 @@ public ExpressionClauseSupport transform * @return */ public DataFormatClauseSupport marshal() { - return CamelDataFormatMessageProcessor.Builder.marshal(); + return CamelDataFormatMessageProcessor.Builder.marshal().camelContext(camelContext); } /** @@ -70,6 +156,6 @@ public DataFormatClauseSupport marshal( * @return */ public DataFormatClauseSupport unmarshal() { - return CamelDataFormatMessageProcessor.Builder.unmarshal(); + return CamelDataFormatMessageProcessor.Builder.unmarshal().camelContext(camelContext); } } diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/EndpointBuilderFactorySupport.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/EndpointBuilderFactorySupport.java new file mode 100644 index 0000000000..18d1370592 --- /dev/null +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/dsl/EndpointBuilderFactorySupport.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 the original author or authors. + * + * 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 com.consol.citrus.camel.dsl; + +import org.apache.camel.builder.endpoint.EndpointBuilderFactory; + +/** + * @author Christoph Deppisch + */ +public class EndpointBuilderFactorySupport implements EndpointBuilderFactory { +} diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelConsumer.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelConsumer.java index 07232f9e56..610aab73e0 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelConsumer.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelConsumer.java @@ -17,6 +17,7 @@ package com.consol.citrus.camel.endpoint; import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; import com.consol.citrus.exceptions.MessageTimeoutException; import com.consol.citrus.message.Message; import com.consol.citrus.messaging.Consumer; @@ -59,13 +60,25 @@ public Message receive(TestContext context) { @Override public Message receive(TestContext context, long timeout) { - String endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + String endpointUri; + if (endpointConfiguration.getEndpointUri() != null) { + endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + } else if (endpointConfiguration.getEndpoint() != null) { + endpointUri = endpointConfiguration.getEndpoint().getEndpointUri(); + } else { + throw new CitrusRuntimeException("Missing endpoint or endpointUri on Camel consumer"); + } if (log.isDebugEnabled()) { log.debug("Receiving message from camel endpoint: '" + endpointUri + "'"); } - Exchange exchange = getConsumerTemplate().receive(endpointUri, timeout); + Exchange exchange; + if (endpointConfiguration.getEndpoint() != null) { + exchange = getConsumerTemplate().receive(endpointConfiguration.getEndpoint(), timeout); + } else { + exchange = getConsumerTemplate().receive(endpointUri, timeout); + } if (exchange == null) { throw new MessageTimeoutException(timeout, endpointUri); diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointBuilder.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointBuilder.java index 89735ada03..170ffe9e41 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointBuilder.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointBuilder.java @@ -19,9 +19,11 @@ package com.consol.citrus.camel.endpoint; +import com.consol.citrus.endpoint.EndpointUriBuilder; import com.consol.citrus.camel.message.CamelMessageConverter; import com.consol.citrus.endpoint.AbstractEndpointBuilder; import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; /** * @author Christoph Deppisch @@ -46,6 +48,26 @@ public CamelEndpointBuilder endpointUri(String endpointUri) { return this; } + /** + * Sets the endpoint uri from given builder. + * @param builder + * @return + */ + public CamelEndpointBuilder endpoint(EndpointUriBuilder builder) { + endpoint.getEndpointConfiguration().setEndpointUri(builder.getUri()); + return this; + } + + /** + * Sets the endpoint property. + * @param camelEndpoint + * @return + */ + public CamelEndpointBuilder endpoint(Endpoint camelEndpoint) { + endpoint.getEndpointConfiguration().setEndpoint(camelEndpoint); + return this; + } + /** * Sets the camelContext property. * @param camelContext diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointConfiguration.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointConfiguration.java index ad5a865aed..7a12759568 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointConfiguration.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelEndpointConfiguration.java @@ -35,6 +35,9 @@ public class CamelEndpointConfiguration extends AbstractEndpointConfiguration { /** Camel endpoint uri */ private String endpointUri; + /** Camel endpoint as destination */ + private Endpoint endpoint; + /** * Gets the Camel context. * @return @@ -67,6 +70,22 @@ public void setEndpointUri(String endpointUri) { this.endpointUri = endpointUri; } + /** + * Gets the endpoint. + * @return + */ + public Endpoint getEndpoint() { + return endpoint; + } + + /** + * Sets the endpoint. + * @param endpoint + */ + public void setEndpoint(Endpoint endpoint) { + this.endpoint = endpoint; + } + /** * Gets the message converter. * @return diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelProducer.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelProducer.java index 22e98d98bc..8252da693d 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelProducer.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelProducer.java @@ -54,19 +54,29 @@ public CamelProducer(String name, CamelEndpointConfiguration endpointConfigurati @Override public void send(final Message message, final TestContext context) { - String endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + String endpointUri; + if (endpointConfiguration.getEndpointUri() != null) { + endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + } else if (endpointConfiguration.getEndpoint() != null) { + endpointUri = endpointConfiguration.getEndpoint().getEndpointUri(); + } else { + throw new CitrusRuntimeException("Missing endpoint or endpointUri on Camel producer"); + } if (log.isDebugEnabled()) { log.debug("Sending message to camel endpoint: '" + endpointUri + "'"); } - Exchange camelExchange = getProducerTemplate() - .send(endpointUri, new Processor() { - @Override - public void process(Exchange exchange) throws Exception { - endpointConfiguration.getMessageConverter().convertOutbound(exchange, message, endpointConfiguration, context); - } - }); + Exchange camelExchange; + if (endpointConfiguration.getEndpoint() != null) { + camelExchange = getProducerTemplate() + .send(endpointConfiguration.getEndpoint(), exchange -> + endpointConfiguration.getMessageConverter().convertOutbound(exchange, message, endpointConfiguration, context)); + } else { + camelExchange = getProducerTemplate() + .send(endpointUri, exchange -> + endpointConfiguration.getMessageConverter().convertOutbound(exchange, message, endpointConfiguration, context)); + } if (camelExchange.getException() != null) { throw new CitrusRuntimeException("Sending message to camel endpoint resulted in exception", camelExchange.getException()); diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncConsumer.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncConsumer.java index 4c7beb9ca8..612cd37242 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncConsumer.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncConsumer.java @@ -20,6 +20,7 @@ import com.consol.citrus.camel.message.CamelMessageHeaders; import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; import com.consol.citrus.exceptions.MessageTimeoutException; import com.consol.citrus.message.Message; import com.consol.citrus.message.MessageHeaders; @@ -60,13 +61,25 @@ public CamelSyncConsumer(String name, CamelSyncEndpointConfiguration endpointCon @Override public Message receive(TestContext context, long timeout) { - String endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + String endpointUri; + if (endpointConfiguration.getEndpointUri() != null) { + endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + } else if (endpointConfiguration.getEndpoint() != null) { + endpointUri = endpointConfiguration.getEndpoint().getEndpointUri(); + } else { + throw new CitrusRuntimeException("Missing endpoint or endpointUri on Camel consumer"); + } if (log.isDebugEnabled()) { log.debug("Receiving message from camel endpoint: '" + endpointUri + "'"); } - Exchange exchange = getConsumerTemplate().receive(endpointUri, timeout); + Exchange exchange; + if (endpointConfiguration.getEndpoint() != null) { + exchange = getConsumerTemplate().receive(endpointConfiguration.getEndpoint(), timeout); + } else { + exchange = getConsumerTemplate().receive(endpointUri, timeout); + } if (exchange == null) { throw new MessageTimeoutException(timeout, endpointUri); diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncEndpointBuilder.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncEndpointBuilder.java index 6f982b051e..146b37d500 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncEndpointBuilder.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncEndpointBuilder.java @@ -23,6 +23,7 @@ import com.consol.citrus.endpoint.AbstractEndpointBuilder; import com.consol.citrus.message.MessageCorrelator; import org.apache.camel.CamelContext; +import org.apache.camel.Endpoint; /** * @author Christoph Deppisch @@ -48,6 +49,16 @@ public CamelSyncEndpointBuilder endpointUri(String endpointUri) { return this; } + /** + * Sets the endpoint property. + * @param camelEndpoint + * @return + */ + public CamelSyncEndpointBuilder endpoint(Endpoint camelEndpoint) { + endpoint.getEndpointConfiguration().setEndpoint(camelEndpoint); + return this; + } + /** * Sets the camelContext property. * @param camelContext diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncProducer.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncProducer.java index 66c896b7cf..ab54495717 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncProducer.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/endpoint/CamelSyncProducer.java @@ -17,6 +17,7 @@ package com.consol.citrus.camel.endpoint; import com.consol.citrus.context.TestContext; +import com.consol.citrus.exceptions.CitrusRuntimeException; import com.consol.citrus.exceptions.ReplyMessageTimeoutException; import com.consol.citrus.message.Message; import com.consol.citrus.message.correlation.CorrelationManager; @@ -61,7 +62,14 @@ public CamelSyncProducer(String name, CamelSyncEndpointConfiguration endpointCon @Override public void send(final Message message, final TestContext context) { - String endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + String endpointUri; + if (endpointConfiguration.getEndpointUri() != null) { + endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + } else if (endpointConfiguration.getEndpoint() != null){ + endpointUri = endpointConfiguration.getEndpoint().getEndpointUri(); + } else { + throw new CitrusRuntimeException("Missing endpoint or endpointUri on Camel producer"); + } if (log.isDebugEnabled()) { log.debug("Sending message to camel endpoint: '" + endpointUri + "'"); @@ -111,7 +119,16 @@ public Message receive(String selector, TestContext context, long timeout) { Message message = correlationManager.find(selector, timeout); if (message == null) { - throw new ReplyMessageTimeoutException(timeout, endpointConfiguration.getEndpointUri()); + String endpointUri; + if (endpointConfiguration.getEndpointUri() != null) { + endpointUri = context.replaceDynamicContentInString(endpointConfiguration.getEndpointUri()); + } else if (endpointConfiguration.getEndpoint() != null) { + endpointUri = endpointConfiguration.getEndpoint().getEndpointUri(); + } else { + throw new CitrusRuntimeException("Missing endpoint or endpointUri on Camel consumer"); + } + + throw new ReplyMessageTimeoutException(timeout, endpointUri); } return message; diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelDataFormatMessageProcessor.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelDataFormatMessageProcessor.java index f3d8a443c6..5998e73cbb 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelDataFormatMessageProcessor.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelDataFormatMessageProcessor.java @@ -67,6 +67,10 @@ public static DataFormatClauseSupport unmarshal() { @Override public CamelDataFormatMessageProcessor doBuild() { + if (dataFormat.getCamelContext() != null) { + camelContext = dataFormat.getCamelContext(); + } + Processor processor; if (operation.equals(DataFormatClause.Operation.Marshal)) { processor = new MarshalProcessor(DataFormatReifier.getDataFormat(camelContext, dataFormat.getDataFormat())); diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelMessageProcessor.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelMessageProcessor.java index 67081e5303..01d5407fc2 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelMessageProcessor.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelMessageProcessor.java @@ -21,6 +21,7 @@ import java.util.Map; +import com.consol.citrus.camel.dsl.CamelContextAware; import com.consol.citrus.context.TestContext; import com.consol.citrus.exceptions.CitrusRuntimeException; import com.consol.citrus.message.Message; @@ -108,7 +109,7 @@ public CamelMessageProcessor doBuild() { } public abstract static class CamelMessageProcessorBuilder> - implements MessageProcessor.Builder, ReferenceResolverAware { + implements MessageProcessor.Builder, ReferenceResolverAware, CamelContextAware { protected CamelContext camelContext; protected ReferenceResolver referenceResolver; @@ -119,6 +120,7 @@ public CamelMessageProcessorBuilder() { self = (B) this; } + @Override public B camelContext(CamelContext camelContext) { this.camelContext = camelContext; return self; diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelRouteProcessor.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelRouteProcessor.java new file mode 100644 index 0000000000..0703c18af4 --- /dev/null +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelRouteProcessor.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 com.consol.citrus.camel.message; + +import java.util.UUID; + +import com.consol.citrus.camel.dsl.CamelContextAware; +import com.consol.citrus.exceptions.CitrusRuntimeException; +import com.consol.citrus.message.MessageProcessor; +import com.consol.citrus.spi.ReferenceResolver; +import com.consol.citrus.spi.ReferenceResolverAware; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.OutputDefinition; +import org.apache.camel.model.RouteDefinition; + +/** + * Message processor builds new route from given processor definition and delegates to the Apache Camel route. + * Sets the message header and body from the processed Camel exchange. + * + * @author Christoph Deppisch + */ +public class CamelRouteProcessor extends CamelMessageProcessor { + + /** + * Constructor initializing camel context and processor. + */ + public CamelRouteProcessor(Builder builder) { + super(builder.camelContext, new RouteProcessor(builder.routeId, builder.routeBuilder)); + + try { + builder.camelContext.addRoutes(builder.routeBuilder); + } catch (Exception e) { + throw new CitrusRuntimeException(String.format("Failed to create route definitions in context '%s'", builder.camelContext.getName()), e); + } + } + + /** + * Fluent builder. + */ + public static class Builder extends OutputDefinition + implements MessageProcessor.Builder, ReferenceResolverAware, CamelContextAware { + + private String routeId = "citrus-" + UUID.randomUUID(); + private RouteBuilder routeBuilder; + + public static Builder route() { + return new Builder(); + } + + public static Builder route(RouteBuilder routeBuilder) { + Builder builder = new Builder(); + builder.routeBuilder = routeBuilder; + return builder; + } + + protected CamelContext camelContext; + protected ReferenceResolver referenceResolver; + + @Override + public Builder routeId(String id) { + this.routeId = id; + return super.routeId(id); + } + + @Override + public Builder camelContext(CamelContext camelContext) { + this.camelContext = camelContext; + return this; + } + + public Builder withReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + return this; + } + + @Override + public final CamelRouteProcessor build() { + if (camelContext == null) { + if (referenceResolver != null) { + camelContext = referenceResolver.resolve(CamelContext.class); + } else { + throw new CitrusRuntimeException("Missing proper Camel context for message processor - " + + "either set explicit context or provide a reference resolver"); + } + } + + if (routeBuilder == null) { + routeBuilder = new RouteBuilder(camelContext) { + @Override + public void configure() throws Exception { + RouteDefinition routeDefinition = from("direct:" + routeId) + .routeId(routeId); + + routeDefinition.setOutputs(Builder.this.getOutputs()); + } + }; + } + + return new CamelRouteProcessor(this); + } + + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + } + } + + /** + * Processor send exchange to given route builder using conventional direct endpoint. + */ + private static class RouteProcessor implements Processor { + private final String routeId; + private final RouteBuilder routeBuilder; + + public RouteProcessor(String routeId, RouteBuilder routeBuilder) { + this.routeId = routeId; + this.routeBuilder = routeBuilder; + } + + @Override + public void process(Exchange exchange) throws Exception { + try { + routeBuilder.getContext().createProducerTemplate() + .send("direct:" + routeId, exchange); + } finally { + routeBuilder.getContext().removeRoute(routeId); + } + } + } +} diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelTransformMessageProcessor.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelTransformMessageProcessor.java index 4be838b15f..c8492405d0 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelTransformMessageProcessor.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/CamelTransformMessageProcessor.java @@ -52,6 +52,13 @@ public static ExpressionClauseSupport transform() { return builder.expression; } + public static ExpressionClauseSupport transform(CamelContext camelContext) { + Builder builder = new Builder(); + builder.expression = new ExpressionClauseSupport<>(builder); + builder.camelContext(camelContext); + return builder.expression; + } + @Override public CamelTransformMessageProcessor doBuild() { TransformProcessor processor; diff --git a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/format/DataFormatClauseSupport.java b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/format/DataFormatClauseSupport.java index 63322c21b7..6fd718d3a1 100644 --- a/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/format/DataFormatClauseSupport.java +++ b/endpoints/citrus-camel/src/main/java/com/consol/citrus/camel/message/format/DataFormatClauseSupport.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +import com.consol.citrus.camel.dsl.CamelContextAware; +import org.apache.camel.CamelContext; import org.apache.camel.builder.DataFormatClause; import org.apache.camel.model.DataFormatDefinition; import org.apache.camel.model.ProcessDefinition; @@ -40,11 +42,21 @@ public class DataFormatClauseSupport { private final DataFormatClause delegate; private final T result; + private CamelContext camelContext; + public DataFormatClauseSupport(T result, DataFormatClause.Operation operation) { this.delegate = new DataFormatClause<>(new InlineProcessDefinition(), operation); this.result = result; } + public DataFormatClauseSupport camelContext(CamelContext camelContext) { + this.camelContext = camelContext; + if (result instanceof CamelContextAware) { + ((CamelContextAware) result).camelContext(camelContext); + } + return this; + } + public T any23(String baseuri) { delegate.any23(baseuri); return result; @@ -702,6 +714,10 @@ public ProcessDefinition unmarshal(DataFormatDefinition dataFormatType) { } } + public CamelContext getCamelContext() { + return camelContext; + } + public DataFormatDefinition getDataFormat() { return dataFormat; } diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/actions/CreateCamelRouteActionTest.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/actions/CreateCamelRouteActionTest.java index a4ecd1480b..015d2a4f2e 100644 --- a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/actions/CreateCamelRouteActionTest.java +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/actions/CreateCamelRouteActionTest.java @@ -38,8 +38,8 @@ public class CreateCamelRouteActionTest extends AbstractTestNGUnitTest { - private AbstractCamelContext camelContext = Mockito.mock(AbstractCamelContext.class); - private RouteDefinition route = Mockito.mock(RouteDefinition.class); + private final AbstractCamelContext camelContext = Mockito.mock(AbstractCamelContext.class); + private final RouteDefinition route = Mockito.mock(RouteDefinition.class); @Test public void testCreateRouteContext() throws Exception { diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelControlbusIT.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelControlbusIT.java new file mode 100644 index 0000000000..d37f6f1e8b --- /dev/null +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelControlbusIT.java @@ -0,0 +1,88 @@ +/* + * Copyright 2006-2015 the original author or authors. + * + * Licensed 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 com.consol.citrus.camel.integration; + +import com.consol.citrus.annotations.CitrusTest; +import com.consol.citrus.message.MessageType; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.apache.camel.CamelContext; +import org.apache.camel.ServiceStatus; +import org.apache.camel.builder.RouteBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import static com.consol.citrus.actions.ReceiveMessageAction.Builder.receive; +import static com.consol.citrus.actions.SendMessageAction.Builder.send; +import static com.consol.citrus.camel.dsl.CamelSupport.camel; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.direct; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.seda; + +/** + * @author Christoph Deppisch + */ +public class CamelControlbusIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldManageRoutes() throws Exception { + camelContext.addRoutes(new RouteBuilder() { + @Override + public void configure() throws Exception { + from(direct("message")) + .routeId("message-to-words") + .autoStartup(false) + .split().tokenize(" ") + .to(seda("words")); + } + }); + + given(camel().camelContext(camelContext) + .controlBus() + .route("message-to-words") + .status() + .result(ServiceStatus.Stopped)); + + when(camel().camelContext(camelContext) + .controlBus() + .route("message-to-words") + .start()); + + then(camel().camelContext(camelContext) + .controlBus() + .route("message-to-words") + .status() + .result(ServiceStatus.Started)); + + when(send(camel().endpoint(direct("message")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + + then(receive(camel().endpoint(seda("words")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus")); + + then(receive(camel().endpoint(seda("words")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("rocks!")); + } +} diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelDataFormatIT.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelDataFormatIT.java new file mode 100644 index 0000000000..3f76fe2351 --- /dev/null +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelDataFormatIT.java @@ -0,0 +1,61 @@ +/* + * Copyright 2006-2015 the original author or authors. + * + * Licensed 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 com.consol.citrus.camel.integration; + +import com.consol.citrus.annotations.CitrusTest; +import com.consol.citrus.message.MessageType; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.apache.camel.CamelContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import static com.consol.citrus.actions.ReceiveMessageAction.Builder.receive; +import static com.consol.citrus.actions.SendMessageAction.Builder.send; +import static com.consol.citrus.camel.dsl.CamelSupport.camel; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.seda; + +/** + * @author Christoph Deppisch + */ +public class CamelDataFormatIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldApplyDataFormat() { + when(send(camel().endpoint(seda("data")::getUri)) + .message() + .body("Citrus rocks!") + .transform(camel(camelContext) + .marshal() + .base64()) + ); + + then(receive("camel:" + camel().endpoints().seda("data").getUri()) + .transform(camel(camelContext) + .unmarshal() + .base64()) + .transform(camel(camelContext) + .convertBodyTo(String.class)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + } + +} diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelProcessorIT.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelProcessorIT.java new file mode 100644 index 0000000000..255919bdba --- /dev/null +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelProcessorIT.java @@ -0,0 +1,73 @@ +/* + * Copyright 2006-2015 the original author or authors. + * + * Licensed 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 com.consol.citrus.camel.integration; + +import com.consol.citrus.annotations.CitrusTest; +import com.consol.citrus.message.MessageType; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.apache.camel.CamelContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import static com.consol.citrus.actions.ReceiveMessageAction.Builder.receive; +import static com.consol.citrus.actions.SendMessageAction.Builder.send; +import static com.consol.citrus.camel.dsl.CamelSupport.camel; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.seda; + +/** + * @author Christoph Deppisch + */ +public class CamelProcessorIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldProcessSentMessage() { + when(send(camel().endpoint(seda("test")::getUri)) + .message() + .body("Citrus rocks!") + .process(camel(camelContext) + .process(exchange -> exchange + .getMessage() + .setBody(exchange.getMessage().getBody(String.class).toUpperCase()))) + ); + + then(receive(camel().endpoint(seda("test")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("CITRUS ROCKS!")); + } + + @Test + @CitrusTest + public void shouldProcessReceivedMessage() { + when(send(camel().endpoint(seda("test")::getUri)) + .message() + .body("Citrus rocks!")); + + then(receive(camel().endpoint(seda("test")::getUri)) + .process(camel(camelContext) + .process(exchange -> exchange + .getMessage() + .setBody(exchange.getMessage().getBody(String.class).toUpperCase()))) + .message() + .type(MessageType.PLAINTEXT) + .body("CITRUS ROCKS!")); + } +} diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteActionIT.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteActionIT.java new file mode 100644 index 0000000000..1204204017 --- /dev/null +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteActionIT.java @@ -0,0 +1,120 @@ +/* + * Copyright 2006-2015 the original author or authors. + * + * Licensed 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 com.consol.citrus.camel.integration; + +import com.consol.citrus.annotations.CitrusTest; +import com.consol.citrus.message.MessageType; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.apache.camel.CamelContext; +import org.apache.camel.ServiceStatus; +import org.apache.camel.builder.RouteBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import static com.consol.citrus.actions.ReceiveMessageAction.Builder.receive; +import static com.consol.citrus.actions.SendMessageAction.Builder.send; +import static com.consol.citrus.camel.dsl.CamelSupport.camel; +import static com.consol.citrus.container.FinallySequence.Builder.doFinally; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.direct; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.seda; + +/** + * @author Christoph Deppisch + */ +public class CamelRouteActionIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldCreateRoutes() throws Exception { + given(camel().camelContext(camelContext) + .route() + .create(new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:messages") + .split().tokenize(" ") + .to("seda:words"); + } + })); + + when(send(camel().endpoint(direct("messages")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + + then(receive(camel().endpoint(seda("words")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus")); + + then(receive(camel().endpoint(seda("words")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("rocks!")); + } + + @Test + @CitrusTest + public void shouldStartStopRoutes() throws Exception { + given(camel().camelContext(camelContext) + .route() + .create(new RouteBuilder() { + @Override + public void configure() throws Exception { + from(direct("text")) + .routeId("split-text") + .autoStartup(false) + .split().tokenize(" ") + .to(seda("words")); + } + })); + + given(camel().camelContext(camelContext) + .controlBus() + .route("split-text") + .status() + .result(ServiceStatus.Stopped)); + + when(camel().camelContext(camelContext) + .route() + .start("split-text")); + + then(camel().camelContext(camelContext) + .controlBus() + .route("split-text") + .status() + .result(ServiceStatus.Started)); + + when(camel().camelContext(camelContext) + .route() + .stop("split-text")); + + then(camel().camelContext(camelContext) + .controlBus() + .route("split-text") + .status() + .result(ServiceStatus.Stopped)); + + when(doFinally().actions( + camel().camelContext(camelContext) + .route() + .remove("split-text"))); + } +} diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteProcessorIT.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteProcessorIT.java new file mode 100644 index 0000000000..dfee5264fc --- /dev/null +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelRouteProcessorIT.java @@ -0,0 +1,117 @@ +/* + * Copyright 2006-2015 the original author or authors. + * + * Licensed 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 com.consol.citrus.camel.integration; + +import com.consol.citrus.annotations.CitrusTest; +import com.consol.citrus.camel.message.CamelRouteProcessor; +import com.consol.citrus.message.MessageType; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.apache.camel.CamelContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import static com.consol.citrus.actions.CreateVariablesAction.Builder.createVariable; +import static com.consol.citrus.actions.ReceiveMessageAction.Builder.receive; +import static com.consol.citrus.actions.SendMessageAction.Builder.send; +import static com.consol.citrus.camel.dsl.CamelSupport.camel; +import static org.apache.camel.builder.Builder.constant; +import static org.apache.camel.builder.Builder.jsonpath; +import static org.apache.camel.builder.Builder.simple; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.seda; + +/** + * @author Christoph Deppisch + */ +public class CamelRouteProcessorIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldProcessRouteOnSend() { + CamelRouteProcessor.Builder beforeSend = camel(camelContext).route() + .processor() + .setBody(simple("{" + + "\"greeting\": {" + + "\"language\": \"${body}\"" + + "}" + + "}")); + + when(send(camel().endpoint(seda("greetings")::getUri)) + .process(beforeSend) + .message() + .body("EN") + ); + + then(receive("camel:" + camel().endpoints().seda("greetings").getUri()) + .message() + .type(MessageType.PLAINTEXT) + .body("{" + + "\"greeting\": {" + + "\"language\": \"EN\"" + + "}" + + "}")); + } + + @Test + @CitrusTest + public void shouldProcessRouteOnReceive() { + CamelRouteProcessor.Builder beforeReceive = camel(camelContext).route(route -> + route.choice() + .when(jsonpath("$.greeting[?(@.language == 'EN')]")) + .setBody(constant("Hello!")) + .when(jsonpath("$.greeting[?(@.language == 'DE')]")) + .setBody(constant("Hallo!")) + .otherwise() + .setBody(constant("Hi!"))); + + given(createVariable("lang", "EN")); + + when(send(camel().endpoint(seda("greetings")::getUri)) + .message() + .body("{" + + "\"greeting\": {" + + "\"language\": \"${lang}\"" + + "}" + + "}") + ); + + then(receive("camel:" + camel().endpoints().seda("greetings").getUri()) + .process(beforeReceive) + .message() + .type(MessageType.PLAINTEXT) + .body("Hello!")); + + given(createVariable("lang", "DE")); + + when(send(camel().endpoint(seda("greetings")::getUri)) + .message() + .body("{" + + "\"greeting\": {" + + "\"language\": \"${lang}\"" + + "}" + + "}") + ); + + then(receive("camel:" + camel().endpoints().seda("greetings").getUri()) + .process(beforeReceive) + .message() + .type(MessageType.PLAINTEXT) + .body("Hallo!")); + } +} diff --git a/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelTransformIT.java b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelTransformIT.java new file mode 100644 index 0000000000..a9a45c2790 --- /dev/null +++ b/endpoints/citrus-camel/src/test/java/com/consol/citrus/camel/integration/CamelTransformIT.java @@ -0,0 +1,77 @@ +/* + * Copyright 2006-2015 the original author or authors. + * + * Licensed 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 com.consol.citrus.camel.integration; + +import com.consol.citrus.annotations.CitrusTest; +import com.consol.citrus.message.MessageType; +import com.consol.citrus.testng.spring.TestNGCitrusSpringSupport; +import org.apache.camel.CamelContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import static com.consol.citrus.actions.ReceiveMessageAction.Builder.receive; +import static com.consol.citrus.actions.SendMessageAction.Builder.send; +import static com.consol.citrus.camel.dsl.CamelSupport.camel; +import static org.apache.camel.builder.endpoint.StaticEndpointBuilders.seda; + +/** + * @author Christoph Deppisch + */ +public class CamelTransformIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldTransformMessageSent() { + when(send(camel().endpoint(seda("hello")::getUri)) + .message() + .body("{\"message\": \"Citrus rocks!\"}") + .transform( + camel() + .camelContext(camelContext) + .transform() + .jsonpath("$.message")) + ); + + then(receive(camel().endpoint(seda("hello")::getUri)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + } + + @Test + @CitrusTest + public void shouldTransformMessageReceived() { + when(send(camel().endpoint(seda("hello")::getUri)) + .message() + .body("{\"message\": \"Citrus rocks!\"}") + ); + + then(receive(camel().endpoint(seda("hello")::getUri)) + .transform( + camel() + .camelContext(camelContext) + .transform() + .jsonpath("$.message")) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + } + +} diff --git a/pom.xml b/pom.xml index 178bad1704..d8710263f2 100644 --- a/pom.xml +++ b/pom.xml @@ -705,6 +705,11 @@ camel-spring-xml ${apache.camel.version} + + org.apache.camel + camel-endpointdsl + ${apache.camel.version} + diff --git a/src/manual/endpoint-camel.adoc b/src/manual/endpoint-camel.adoc index 299062718f..1a5616449e 100644 --- a/src/manual/endpoint-camel.adoc +++ b/src/manual/endpoint-camel.adoc @@ -1,9 +1,16 @@ [[apache-camel]] = Apache Camel support -Apache Camel project implements the enterprise integration patterns for building mediation and routing rules in your enterprise application. With the Citrus Camel support you are able to directly interact with the Apache Camel components and route definitions. You can call Camel routes and receive synchronous response messages. You can also simulate the Camel route endpoint with receiving messages and providing simulated response messages. +[Apache Camel](https://camel.apache.org) is a fantastic Open Source integration framework that empowers you to quickly +and easily integrate various systems consuming or producing data. -NOTE: The camel components in Citrus are kept in a separate Maven module. So you should add the module as Maven dependency to your project accordingly. +Camel implements the enterprise integration patterns for building mediation and routing rules in your software. +With the Camel support in Citrus you are able to directly interact with the 300+ Camel components through route definitions. +You can call Camel routes and receive synchronous response messages from a Citrus test. +You can also simulate the Camel route endpoint with receiving messages and providing simulated response messages. + +NOTE: The camel components in Citrus are located in a separate Maven module. +So you should add the module as Maven dependency to your project accordingly. [source,xml] ---- @@ -14,8 +21,10 @@ NOTE: The camel components in Citrus are kept in a separate Maven module. So you ---- -Citrus provides a special Apache Camel configuration schema that is used in our Spring configuration files. You have to include the citrus-camel namespace in your Spring configuration XML files as follows. +For XML configurations Citrus provides a special Camel configuration schema that is used in Spring configuration files. +You have to include the `citrus-camel` namespace in your Spring configuration XML files as follows. +.XML [source,xml] ---- ---- -Now you are ready to use the Citrus Apache Camel configuration elements using the citrus-camel namespace prefix. +Now you are ready to use the Camel configuration elements using the citrus-camel namespace prefix. The next sections explain the Citrus capabilities while working with Apache Camel. [[camel-endpoint]] == Camel endpoint -Camel and Citrus both use the endpoint pattern in order to define message destinations. Users can interact with these endpoints when creating the mediation and routing logic. The Citrus endpoint component for Camel interaction is defined as follows in your Citrus Spring configuration. +Camel and Citrus both use the endpoint pattern in order to define message destinations. +Users can interact with these endpoints when creating the mediation and routing logic. +The Citrus endpoint component for Camel interaction is defined as follows in your Citrus Spring configuration. -[source,xml] +.Java +[source,java,indent=0,role="primary"] ---- - +@Bean +public CamelEndpoint helloCamelEndpoint() { + return new CamelEndpoint() + .endpointUri("direct:hello") + .build(); +} ---- -Right next to that Citrus endpoint we need the Apache Camel route that is located inside a camel context component. +.XML +[source,xml,indent=0,role="secondary"] +---- + +---- -[source,xml] +The endpoint defines an endpoint uri that will be called when messages are sent/received using the endpoint producer or consumer. +The endpoint uri refers to a Camel route that is located inside a Camel context. +The Camel route should use the respective endpoint uri as `from` definition. + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public CamelContext camelContext() { + CamelContext context = new DefaultCamelContext(); + + context.addRoutes(new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:hello") + .routeId("helloRoute") + .to("log:com.consol.citrus.camel?level=INFO") + .to("seda:greetings"); + } + }); + + return context; +} ---- +.XML +[source,xml,indent=0,role="secondary"] +---- - - + + - + ---- -As you can see the Citrus camel endpoint is able to interact with the Camel route. In the example above the Camel context is placed as Spring bean Camel context. +In the example above the Camel context is placed as Spring bean. +The context defines a Camel route the Citrus test case can interact with. -The Camel context is automatically referenced in the Citrus Camel endpoint. This is because Citrus will automatically look for a Camel context in the -Spring bean configuration. +The Camel context is automatically referenced in the Citrus Camel endpoint. +This is because Citrus will automatically look for a Camel context in the Spring bean configuration. -In case you have multiple Camel context instances in your configuration you can explicitly link the endpoint to a context with `camel-context="camelContext"`. +In case you have multiple Camel context instances in your configuration you can explicitly link the endpoint to a +context with `camel-context="camelContext"`. -[source,xml] +.Java +[source,java,indent=0,role="primary"] ---- - +@Bean +public CamelEndpoint helloCamelEndpoint() { + return new CamelEndpoint() + .camelContext(specialCamelContext) + .endpointUri("direct:hello") + .build(); +} ---- -This explicitly binds the endpoint to the context named "_camelContext_". This configuration would be the easiest setup to use Camel with Citrus as you can add the Camel context straight to the Spring bean application context and interact with it in Citrus. Of course you can also import your Camel context and routes from other Spring bean context files or you can start the Camel context routes with Java code. +.XML +[source,xml,indent=0,role="secondary"] +---- + +---- -In the example the Apache Camel route is listening on the route endpoint uri *direct:news* . Incoming messages will be logged to the console using a *log* Camel component. After that the message is forwarded to a *seda* Camel component which is a simple queue in memory. So we have a small Camel routing logic with two different message transports. +This explicitly binds the endpoint to the context named "_specialCamelContext_". +This configuration would be the easiest setup to use Camel with Citrus as you can add the Camel context straight to the +Spring bean application context and interact with it in Citrus. +Of course, you can also import your Camel context and routes from other Spring bean context files, +or you can start the Camel context routes with Java code. -The Citrus endpoint can interact with this sample route definition. The endpoint configuration holds the endpoint uri information that tells Citrus how to access the Apache Camel route destination. This endpoint uri can be any Camel endpoint uri that is used in a Camel route. Here we just use the direct endpoint uri *direct:news* so the sample Camel route gets called directly. In your test case you can use this endpoint component referenced by its id or name in order to send and receive messages on the route address *direct:news* . The Camel route listening on this direct address will be invoked accordingly. +In the example the Camel route is listening on the route endpoint uri `direct:hello`. +Incoming messages will be logged to the console using a `log` Camel component. +After that the message is forwarded to a `seda` Camel component which is a simple queue in memory. -The Apache Camel routes support asynchronous and synchronous message communication patterns. By default Citrus uses asynchronous communication with Camel routes. This means that the Citrus producer sends the exchange message to the route endpoint uri and is finished immediately. There is no synchronous response to await. In contrary to that the synchronous endpoint will send and receive a synchronous message on the Camel destination route. We will discuss this later on in this chapter. For now we have a look on how to use the Citrus camel endpoint in a test case in order to send a message to the Camel route: +The Citrus endpoint can interact with this sample route definition sending messages to the `direct:hello` endpoint. +The endpoint configuration holds the endpoint uri information that tells Citrus how to access the Camel route. +This endpoint uri can be any Camel endpoint uri that is used in a Camel route. -[source,xml] +The Camel routes support asynchronous and synchronous message communication patterns. +By default, Citrus uses asynchronous communication with Camel routes. +This means that the Citrus producer sends the exchange message to the route endpoint uri and is finished immediately. +There is no synchronous response to await. +In contrary to that the synchronous endpoint will send and receive a synchronous message on the Camel destination route. +This message exchange pattern is discussed in a later section in this chapter. + +For now, we have a look on how to use the Citrus Camel endpoint in a test case in order to send a message to the Camel route: + +.Java +[source,java,indent=0,role="primary"] +---- +send(helloCamelEndpoint) + .message() + .body("Hello from Citrus!"); +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- - + Hello from Citrus! ---- -The Citrus camel endpoint component can also be used in a receive message action in your test case. In this situation you would receive a message from the route endpoint. This is especially designed for queueing endpoint routes such as the Camel seda component. In our example Camel route above the seda Camel component is called with the endpoint uri *seda:news-feed* . This means that the Camel route is sending a message to the seda component. Citrus is able to receive this route message with a endpoint component like this: +You can use the very same Citrus Camel endpoint component to receive messages in your test case, too. +In this situation you would receive a message from the route endpoint. +This is especially designed for queueing endpoint routes such as the Camel seda component. +In our example Camel route above the seda Camel component is called with the endpoint uri *seda:greetings-feed*. -[source,xml] +This means that the Camel route is sending a message to the `seda` component. +Citrus is able to receive this route message with an endpoint component like this: + +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public CamelEndpoint greetingsFeed() { + return new CamelEndpoint() + .endpointUri("seda:greetings-feed") + .build(); +} +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- - + ---- You can use the Citrus camel endpoint in your test case receive action in order to consume the message on the seda component. -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +receive(greetingsFeed) + .message() + .type(MessageType.PLAINTEXT) + .body("Hello from Citrus!"); ---- - + +.XML +[source,xml,indent=0,role="secondary"] +---- + Hello from Citrus! ---- -TIP: Instead of defining a static Citrus camel component you could also use the dynamic endpoint components in Citrus. This would enable you to send your message directly using the endpoint uri *direct:news* in your test case. Read more about this in link:#dynamic-endpoint-components[dynamic-endpoint-components]. +TIP: Instead of defining a static Citrus camel component you could also use the dynamic endpoint components in Citrus. +This would enable you to send your message directly using the endpoint uri *direct:news* in your test case. +Read more about this in link:#dynamic-endpoint-components[dynamic-endpoint-components]. -Citrus is able to send and receive messages with Camel route endpoint uri. This enables you to invoke a Camel route. The Camel components used is defined by the endpoint uri as usual. When interacting with Camel routes you might need to send back some response messages in order to simulate boundary applications. We will discuss the synchronous communication in the next section. +Citrus is able to send and receive messages with Camel route endpoint uri. +This enables you to invoke a Camel route. +The Camel components used is defined by the endpoint uri as usual. +When interacting with Camel routes you might need to send back some response messages in order to simulate boundary applications. +We will discuss the synchronous communication in the next section. [[synchronous-camel-endpoint]] == Synchronous Camel endpoint -The synchronous Apache Camel producer sends a message to some route and waits synchronously for the response to arrive. In Camel this communication is represented with the exchange pattern *InOut* . The basic configuration for a synchronous Apache Camel endpoint component looks like follows: +The synchronous Camel producer sends a message to a route and waits synchronously for the response to arrive. +In Camel this communication is represented with the exchange pattern *InOut*. +The basic configuration for a synchronous Camel endpoint component looks like follows: -[source,xml] +.Java +[source,java,indent=0,role="primary"] ---- - ---- -Synchronous endpoints poll for synchronous reply messages to arrive. The poll interval is an optional setting in order to manage the amount of reply message handshake attempts. Once the endpoint was able to receive the reply message synchronously the test case can receive the reply. In case the reply message is not available in time we raise some timeout error and the test will fail. +Synchronous endpoints poll for synchronous reply messages to arrive. +The poll interval is an optional setting in order to manage the amount of reply message handshake attempts. +Once the endpoint was able to receive the reply message synchronously the test case can receive the reply. +In case the reply message is not available in time we raise some timeout error and the test will fail. -In a first test scenario we write a test case the sends a message to the synchronous endpoint and waits for the synchronous reply message to arrive. So we have two actions on the same Citrus endpoint, first send then receive. +In a first test scenario we write a test case the sends a message to the synchronous endpoint and waits for the synchronous +reply message to arrive. +So we have two actions on the same Citrus endpoint, first send then receive. -[source,xml] +.Java +[source,java,indent=0,role="primary"] ---- - +send(helloCamelEndpoint) + .message() + .type(MessageType.PLAINTEXT) + .body("Hello from Citrus!"); + +receive(helloCamelEndpoint) + .message() + .type(MessageType.PLAINTEXT) + .body("This is the reply from Apache Camel!"); +---- + +.XML +[source,xml,indent=0,role="secondary"] +---- + Hello from Citrus! - + This is the reply from Apache Camel! ---- -The next variation deals with the same synchronous communication, but send and receive roles are switched. Now Citrus receives a message from a Camel route and has to provide a reply message. We handle this synchronous communication with the same synchronous Apache Camel endpoint component. Only difference is that we initially start the communication by receiving a message from the endpoint. Knowing this Citrus is able to send a synchronous response back. Again just use the same endpoint reference in your test case. So we have again two actions in our test case, but this time first receive then send. +The next variation deals with the same synchronous communication, but send and receive roles are switched. +Now Citrus receives a message from a Camel route and has to provide a reply message. +We handle this synchronous communication with the same synchronous Apache Camel endpoint component. +Only difference is that we initially start the communication by receiving a message from the endpoint. +Knowing this Citrus is able to send a synchronous response back. +Again just use the same endpoint reference in your test case. +So we have again two actions in our test case, but this time first receive then send. -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +receive(helloCamelEndpoint) + .message() + .type(MessageType.PLAINTEXT) + .body("Hello from Apache Camel!"); + +send(helloCamelEndpoint) + .message() + .type(MessageType.PLAINTEXT) + .body("This is the reply from Citrus!"); ---- - + +.XML +[source,xml,indent=0,role="secondary"] +---- + Hello from Apache Camel! - + This is the reply from Citrus! ---- -This is pretty simple. Citrus takes care on setting the Apache Camel exchange pattern *InOut* while using synchronous communications. The Camel routes do respond and Citrus is able to receive the synchronous messages accordingly. With this pattern you can interact with Apache Camel routes where Citrus simulates synchronous clients and consumers. +This is pretty simple. Citrus takes care on setting the Camel exchange pattern *InOut* while using synchronous communications. +The Camel routes do respond and Citrus is able to receive the synchronous messages accordingly. +With this pattern you can interact with Camel routes where Citrus simulates synchronous clients and consumers. [[camel-exchange-headers]] == Camel exchange headers -Apache Camel uses exchanges when sending and receiving messages to and from routes. These exchanges hold specific information on the communication outcome. Citrus automatically converts these exchange information to special message header entries. You can validate those exchange headers then easily in your test case: +Camel uses exchanges when sending and receiving messages to and from routes. +These exchanges hold specific information on the communication outcome. +Citrus automatically converts these exchange information to special message header entries. +You can validate those exchange headers then easily in your test case: -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +receive(greetingsFeed) + .message() + .type(MessageType.PLAINTEXT) + .body("Hello from Camel!") + .header("citrus_camel_route_id", "greetings") + .header("citrus_camel_exchange_id", "ID-local-50532-1402653725341-0-3") + .header("citrus_camel_exchange_failed", false) + .header("citrus_camel_exchange_pattern", "InOnly") + .header("CamelCorrelationId", "ID-local-50532-1402653725341-0-1") + .header("CamelToEndpoint", "seda://greetings-feed"); +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- - + Hello from Camel!
- + - +
---- -Besides the Camel specific exchange information the Camel exchange does also hold some custom properties. These properties such as *CamelToEndpoint* or *CamelCorrelationId* are also added automatically to the Citrus message header so can expect them in a receive message action. +In addition to the Camel specific exchange information the Camel exchange does also hold some custom properties. +These properties such as *CamelToEndpoint* or *CamelCorrelationId* are also added automatically to the Citrus message header so can expect them in a `receive` message action. [[camel-exception-handling]] == Camel exception handling Let us suppose following route definition: -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +@Bean +public CamelContext camelContext() { + CamelContext context = new DefaultCamelContext(); + + context.addRoutes(new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:hello") + .routeId("helloRoute") + .to("log:com.consol.citrus.camel?level=INFO") + .to("seda:greetings-feed") + .onException(CitrusRuntimeException.class) + .to("seda:exceptions"); + } + }); + + return context; +} +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- - - + + - + com.consol.citrus.exceptions.CitrusRuntimeException @@ -213,11 +435,25 @@ Let us suppose following route definition: ---- -The route has an exception handling block defined that is called as soon as the exchange processing ends up in some error or exception. With Citrus you can also simulate a exchange exception when sending back a synchronous response to a calling route. +The route has an exception handling block defined that is called as soon as the exchange processing ends up in some error or exception. +With Citrus you can also simulate a exchange exception when sending back a synchronous response to a calling route. -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +send(helloCamelEndpoint) + .message() + .type(MessageType.PLAINTEXT) + .body("Something went wrong!") + .header("citrus_camel_exchange_exception", CitrusRuntimeException.class) + .header("citrus_camel_exchange_exception_message", "Something went wrong!") + .header("citrus_camel_exchange_failed", true); +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- - + Something went wrong! @@ -230,18 +466,34 @@ The route has an exception handling block defined that is called as soon as the ---- -This message as response to the *seda:news-feed* route would cause Camel to enter the exception handling in the route definition. The exception handling is activated and calls the error handling route endpoint *seda:exceptions* . Of course Citrus would be able to receive such an exception exchange validating the exception handling outcome. +This message as response to the *seda:greetings-feed* route would cause Camel to enter the exception handling in the route definition. +The exception handling is activated and calls the error handling route endpoint *seda:exceptions* . +Of course Citrus would be able to receive such an exception exchange validating the exception handling outcome. -In such failure scenarios the Apache Camel exchange holds the exception information (*CamelExceptionCaught*) such as causing exception class and error message. These headers are present in an error scenario and can be validated in Citrus when receiving error messages as follows: +In such failure scenarios the Camel exchange holds the exception information (*CamelExceptionCaught*) such as causing exception class and error message. +These headers are present in an error scenario and can be validated in Citrus when receiving error messages as follows: -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +receive(errorCamelEndpoint) + .message() + .type(MessageType.PLAINTEXT) + .body("Something went wrong!") + .header("citrus_camel_route_id", "helloRoute") + .header("citrus_camel_exchange_failed", true) + .header("CamelExceptionCaught", "com.consol.citrus.exceptions.CitrusRuntimeException: Something went wrong!"); +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- Something went wrong!
- + @@ -249,16 +501,66 @@ In such failure scenarios the Apache Camel exchange holds the exception informat ---- -This completes the basic exception handling in Citrus when using the Apache Camel endpoints. +This completes the basic exception handling in Citrus when using the Camel endpoints. [[camel-context-handling]] == Camel context handling -In the previous samples we have used the Apache Camel context as Spring bean context that is automatically loaded when Citrus starts up. Now when using a single Camel context instance Citrus is able to automatically pick this Camel context for route interaction. If you use more that one Camel context you have to tell the Citrus endpoint component which context to use. The endpoint offers an optional attribute called *camel-context* . +In the previous samples we have used the Camel context as Spring bean context that is automatically loaded when Citrus starts up. +Now when using a single Camel context instance Citrus is able to automatically pick this Camel context for route interaction. +If you use more than one Camel context you have to tell the Citrus endpoint component which context to use. +The endpoint offers an optional attribute called `camel-context`. -[source,xml] +.Java +[source,java,indent=0,role="primary"] ---- - @@ -274,18 +576,23 @@ In the previous samples we have used the Apache Camel context as Spring bean con - + ---- -In the example abpove we have two Camel context instances loaded. The endpoint has to pick the context to use with the attribute *camel-context* which resides to the Spring bean id of the Camel context. +In the example above we have two Camel context instances loaded. +The endpoint has to pick the context to use with the attribute *camel-context* which resides to the Spring bean id of the Camel context. [[camel-route-actions]] == Camel route actions -Since Citrus 2.4 we introduced some Camel specific test actions that enable easy interaction with Camel routes and the Camel context. The test actions do follow a specific XML namespace so we have to add this namespace to the test case when using the actions. +Since Citrus 2.4 we introduced some Camel specific test actions that enable easy interaction with Camel routes and the Camel context. +NOTE: In XML the Camel route test actions do follow a specific XML namespace. +This means you have to add this namespace to the test case when using the actions. + +.XML [source,xml] ---- ---- -We added a special camel namespace with prefix *camel:* so now we can start to add Camel test actions to the test case: +Once you have added the special Camel namespace with prefix `camel` you are ready to start using the Camel test actions in your test case. -.XML DSL -[source,xml] +[camel-route-create] +=== Create Camel routes + +You can create a new Camel route as part of the test using this test action. + +.Java +[source,java,indent=0,role="primary"] +---- +public class CamelRouteActionIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void createCamelRoute() { + $(camel().camelContext(camelContext) + .route() + .create(new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:messages") + .routeId("message-tokenizer") + .split().tokenize(" ") + .to("seda:words"); + } + })); + } +} +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- - - - - - - - - - - - - - - - - - + + + + + + @@ -335,16 +662,41 @@ We added a special camel namespace with prefix *camel:* so now we can start to a ---- -In the example above we have used the *camel:create-route* test action that will create new Camel routes at runtime in the Camel context. The target Camel context is referenced with an automatically context lookup. The default Camel context name in this lookup is "_citrusCamelContext_". -If no specific settings are set Citrus will automatically try to look up the Camel context with name "_citrusCamelContext_" in the Spring bean configuration. All -route operations will target this Camel context then. +In the example above we have used the *camel:create-route* test action that will create new Camel routes at runtime in the Camel context. +The target Camel context is referenced with an automatic context lookup. + +NOTE: The default Camel context name in this lookup is "_citrusCamelContext_". + +If no specific settings are set Citrus will automatically try to look up the Camel context with name "_citrusCamelContext_" in the Spring bean configuration. +All route operations will target this Camel context then. In addition to that you can skip this lookup and directly reference a target Camel context with the action attribute *camel-context* (used in the second action above). -Removing routes at runtime is also supported. +[camel-route-remove] +=== Remove Camel routes -.XML DSL -[source,xml] +You can remove routes from the Camel context as part of the test. + +.Java +[source,java,indent=0,role="primary"] +---- +public class CamelRouteActionIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void createCamelRoute() { + $(camel().camelContext(camelContext) + .route() + .remove("route_1", "route_2", "route_3")); + } +} +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- @@ -357,10 +709,35 @@ Removing routes at runtime is also supported. ---- +[camel-route-start-stop] +=== Start/stop routes + Next operation we will discuss is the start and stop of existing Camel routes: -.XML DSL -[source,xml] +.Java +[source,java,indent=0,role="primary"] +---- +public class CamelRouteActionIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void createCamelRoute() { + $(camel().camelContext(camelContext) + .route() + .start("route_1")); + + $(camel().camelContext(camelContext) + .route() + .stop("route_2", "route_3")); + } +} +---- + +.XML +[source,xml,indent=0,role="secondary"] ---- @@ -376,62 +753,64 @@ Next operation we will discuss is the start and stop of existing Camel routes: ---- -Starting and stopping Camel routes at runtime is important when temporarily Citrus need to receive a message on a Camel endpoint URI. We can stop a route, use a Citrus camel endpoint instead for validation and start the route after the test is done. This way wen can also simulate errors and failure scenarios in a Camel route interaction. - -Of course all Camel route actions are also available in Java DSL. - -.Java DSL -[source,java] ----- -@Autowired -private CamelContext camelContext; - -@CitrusTest -public void camelRouteTest() { - camel().context(camelContext).create(new RouteBuilder(camelContext) { - @Override - public void configure() throws Exception { - from("direct:news") - .routeId("route_1") - .autoStartup(false) - .setHeader("headline", simple("This is BIG news!")) - .to("mock:news"); - - from("direct:rumors") - .routeId("route_2") - .autoStartup(false) - .setHeader("headline", simple("This is just a rumor!")) - .to("mock:rumors"); - } - }); +Starting and stopping Camel routes at runtime is important when temporarily Citrus need to receive a message on a Camel endpoint URI. +We can stop a route, use a Citrus camel endpoint instead for validation and start the route after the test is done. +This way wen can also simulate errors and failure scenarios in a Camel route interaction. - camel().context(camelContext).start("route_1", "route_2"); +[[camel-controlbus-actions]] +== Camel controlbus actions - camel().context(camelContext).stop("route_2"); +The Camel controlbus component is a good way to access route statistics and route status information within a Camel context. +Citrus provides controlbus test actions to easily access the controlbus operations at runtime. - camel().context(camelContext).remove("route_2"); +.Java +[source,java,indent=0,role="primary"] +---- +public class CamelControlBusIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void createCamelRoute() { + $(camel().camelContext(camelContext) + .controlbus() + .route("route_1") + .status() + .result(ServiceStatus.Stopped)); + + $(camel().camelContext(camelContext) + .controlbus() + .route("route_1") + .start()); + + $(camel().camelContext(camelContext) + .controlbus() + .route("route_1") + .status() + .result(ServiceStatus.Started)); + } } ---- -As you can see we have access to the Camel route builder that adds `n-1` new Camel routes to the context. After that we can start, stop and remove the routes within the test case. - -[[camel-controlbus-actions]] -== Camel controlbus actions - -The Camel controlbus component is a good way to access route statistics and route status information within a Camel context. Citrus provides controlbus test actions to easily access the controlbus operations at runtime. - -.XML DSL -[source,xml] +.XML +[source,xml,indent=0,role="secondary"] ---- + + + Stopped + + - - Stopped + + Started @@ -446,29 +825,262 @@ The Camel controlbus component is a good way to access route statistics and rout ---- -The example test case shows the controlbus access. As already mentioned you can explicitly reference a target Camel context with `camel-context="camelContext"`. In case no specific context is referenced Citrus will automatically lookup a target Camel context with the default context name "_citrusCamelContext_". +The example test case shows the controlbus access. +As already mentioned you can explicitly reference a target Camel context with `camel-context="camelContext"`. +In case no specific context is referenced Citrus will automatically lookup a target Camel context with the default context name "_citrusCamelContext_". -Camel provides two different ways to specify operations and parameters. The first option is the use of an *action* attribute. The Camel route id has to be specified as mandatory attribute. As a result the controlbus action will be executed on the target route during test runtime. This way we can also start and stop Camel routes in a Camel context. +Camel provides two different ways to specify operations and parameters. +The first option is the use of an *action* attribute. +The Camel route id has to be specified as mandatory attribute. +As a result the controlbus action will be executed on the target route during test runtime. +This way we can also start and stop Camel routes in a Camel context. -In case an controlbus operation has a result such as the *status* action we can specify a control result that is compared. Citrus will raise validation exceptions when the results differ. The second option for executing a controlbus action is the language expression. We can use Camel language expressions on the Camel context for accessing a controlbus operation. Also here we can define an optional outcome as expected result. +In case a controlbus operation has a result such as the *status* action we can specify a control result that is compared. +Citrus will raise validation exceptions when the results differ. -The Java DSL also supports these controlbus operations as the next example shows: +The second option for executing a controlbus action is the language expression. +We can use Camel language expressions on the Camel context for accessing a controlbus operation. +Also, here we can define an optional outcome as expected result. -.Java DSL -[source,java] +.Java +[source,java,indent=0,role="primary"] +---- +public class CamelControlBusIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void createCamelRoute() { + $(camel().camelContext(camelContext) + .controlbus() + .language(SimpleBuilder.simple("${camelContext.getRouteStatus('my_route')}")) + .result(ServiceStatus.Stopped)); + + $(camel().camelContext(camelContext) + .controlbus() + .language(SimpleBuilder.simple("${camelContext.stop()}"))); + } +} ---- -@Autowired -private CamelContext camelContext; -@CitrusTest -public void camelRouteTest() { - camel().controlBus() - .route("my_route", "start"); +.XML +[source,xml,indent=0,role="secondary"] +---- + + + + ${camelContext.getRouteStatus('my_route')} + Started + + + + ${camelContext.stop()} + + + +---- + +[[camel-endpoint-dsl]] +== Camel endpoint DSL + +Since Camel 3 the endpoint DSL provides a convenient way to construct an endpoint uri. +In Citrus you can use the Camel endpoint DSL to send/receive messages in a test. + +.Java +[source,java,indent=0,role="primary"] +---- +$(send(camel().endpoint(seda("test")::getUri)) + .message() + .body("Citrus rocks!")); +---- + +The fluent endpoint DSL in Camel allows to build the endpoint uri. +The `camel().endpoint(seda("test")::getUri)` builds the endpoint uri `seda:test`. +The endpoint DSL provides all settings and properties that you can set for a Camel endpoint component. + +[[camel-processor-support]] +== Camel processor support + +Camel implements the concept of processors as enterprise integration pattern. +A processor is able to add custom logic to a Camel route. +Each processor is able to access the Camel exchange that is being processed in the current route. +The processor is able to change the message content (body, headers) as well as the exchange information. + +The send/receive operations in Citrus also implement the processor concept. +With the Citrus Camel support you can use the very same Camel processor also in a Citrus test action. + +.Message processor on send +[source,java,indent=0,role="primary"] +---- +public class CamelMessageProcessorIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldProcessMessages() { + $(send(camel().endpoint(seda("test")::getUri)) + .message() + .body("Citrus rocks!") + .process(camel(camelContext) + .process(exchange -> exchange + .getMessage() + .setBody(exchange.getMessage().getBody(String.class).toUpperCase()))) + ); + } +} +---- + +The example above uses a Camel processor to change the exchange and the message content before the message is sent to the endpoint. +This way you can apply custom changes to the message as part of the test action. + +.Message processor on receive +[source,java,indent=0,role="primary"] +---- +public class CamelMessageProcessorIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldProcessMessages() { + $(send(camel().endpoint(seda("test")::getUri)) + .message() + .body("Citrus rocks!")); + + $(receive(camel().endpoint(seda("test")::getUri)) + .process(camel(camelContext) + .process(exchange -> exchange + .getMessage() + .setBody(exchange.getMessage().getBody(String.class).toUpperCase()))) + .message() + .type(MessageType.PLAINTEXT) + .body("CITRUS ROCKS!")); + } +} +---- + +The Camel processors are very powerful. +In particular, you can apply transformations of multiple kind. + +.Transform processor +[source,java,indent=0,role="primary"] +---- +public class CamelTransformIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldTransformMessageReceived() { + $(send(camel().endpoint(seda("hello")::getUri)) + .message() + .body("{\"message\": \"Citrus rocks!\"}") + ); + + $(receive(camel().endpoint(seda("hello")::getUri)) + .transform( + camel() + .camelContext(camelContext) + .transform() + .jsonpath("$.message")) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + } +} +---- - camel().controlBus() - .language(SimpleBuilder.simple("${camelContext.getRouteStatus('my_route')}")) - .result(ServiceStatus.Started); +The transform pattern is able to change the message content before a message is received/sent in Citrus. +The example above applies a JsonPath expression as part of the message processing. +The JsonPath expression evaluates `$.message` on the Json payload and saves the result as new message body content. +The following message validation expects the plaintext value `Citrus rocks!`. + +The message processor is also able to apply a complete route logic as part of the test action. + +.Route processor +[source,java,indent=0,role="primary"] +---- +public class CamelRouteProcessorIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldProcessRoute() { + CamelRouteProcessor.Builder beforeReceive = camel(camelContext).route(route -> + route.choice() + .when(jsonpath("$.greeting[?(@.language == 'EN')]")) + .setBody(constant("Hello!")) + .when(jsonpath("$.greeting[?(@.language == 'DE')]")) + .setBody(constant("Hallo!")) + .otherwise() + .setBody(constant("Hi!"))); + + $(send(camel().endpoint(seda("greetings")::getUri)) + .message() + .body("{" + + "\"greeting\": {" + + "\"language\": \"EN\"" + + "}" + + "}") + ); + + $(receive("camel:" + camel().endpoints().seda("greetings").getUri()) + .process(beforeReceive) + .message() + .type(MessageType.PLAINTEXT) + .body("Hello!")); + } +} +---- + +With the complete route logic you have the full power of Camel ready to be used in your send/receive test action. +This enables many capabilities as Camel implements the enterprise integration patterns such as split, choice, enrich and many more. + +[[camel-data-format-support]] +== Camel data format support + +Camel uses the concept of data format to transform message content in form of marshal/unmarshal operations. +You can use the data formats supported in Camel in Citrus, too. + +.Data format marshal/unmarshal +[source,java,indent=0,role="primary"] +---- +public class CamelDataFormatIT extends TestNGCitrusSpringSupport { + + @Autowired + private CamelContext camelContext; + + @Test + @CitrusTest + public void shouldApplyDataFormat() { + when(send(camel().endpoint(seda("data")::getUri)) + .message() + .body("Citrus rocks!") + .transform(camel(camelContext) + .marshal() + .base64()) + ); + + then(receive("camel:" + camel().endpoints().seda("data").getUri()) + .transform(camel(camelContext) + .unmarshal() + .base64()) + .transform(camel(camelContext) + .convertBodyTo(String.class)) + .message() + .type(MessageType.PLAINTEXT) + .body("Citrus rocks!")); + } } ---- -The Java DSL works with Camel language expression builders as well as *ServiceStatus* enum values as expected result. +The example above uses the `base64` data format provided in Camel to marshal/unmarshal the message content to/from a base64 encoded String. +Camel provides support for many data formats as you can see in the [documentation on data formats](https://camel.apache.org/components/latest/dataformats/index.html). diff --git a/vintage/citrus-java-dsl/src/main/java/com/consol/citrus/dsl/builder/CamelRouteActionBuilder.java b/vintage/citrus-java-dsl/src/main/java/com/consol/citrus/dsl/builder/CamelRouteActionBuilder.java index c9b8f7175c..60b9fb4cc5 100644 --- a/vintage/citrus-java-dsl/src/main/java/com/consol/citrus/dsl/builder/CamelRouteActionBuilder.java +++ b/vintage/citrus-java-dsl/src/main/java/com/consol/citrus/dsl/builder/CamelRouteActionBuilder.java @@ -1,5 +1,6 @@ package com.consol.citrus.dsl.builder; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -22,6 +23,7 @@ public class CamelRouteActionBuilder implements TestActionBuilder.DelegatingTest private final com.consol.citrus.camel.actions.CamelRouteActionBuilder delegate = new com.consol.citrus.camel.actions.CamelRouteActionBuilder(); + private final List routeIds = new ArrayList<>(); public CamelRouteActionBuilder context(String camelContext) { delegate.context(camelContext); @@ -38,7 +40,7 @@ public CamelRouteActionBuilder routes(String... routeIds) { } public CamelRouteActionBuilder routes(List routeIds) { - delegate.routes(routeIds); + this.routeIds.addAll(routeIds); return this; } @@ -55,15 +57,18 @@ public CamelControlBusAction.Builder controlBus() { } public StartCamelRouteAction.Builder start(String ... routes) { - return delegate.start(routes); + return delegate.start(routes) + .routeIds(routeIds); } public StopCamelRouteAction.Builder stop(String ... routes) { - return delegate.stop(routes); + return delegate.stop(routes) + .routeIds(routeIds); } public RemoveCamelRouteAction.Builder remove(String ... routes) { - return delegate.remove(routes); + return delegate.remove(routes) + .routeIds(routeIds); } public CamelRouteActionBuilder withReferenceResolver(ReferenceResolver referenceResolver) {