This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-25928-eip-body-types-outside-core in repository https://gitbox.apache.org/repos/asf/camel.git
commit 20525c1ee2eab244c8974b2c7d838c7393de0ae7 Author: Claus Ibsen <[email protected]> AuthorDate: Sun Oct 25 13:58:02 2026 +0100 CAMEL-25928: runtime checks of the body types of the EIPs implemented outside camel-core As EipBodyTypesTest does for the EIPs of core, a test in each module that implements an EIP shows its body types hold when the route runs: circuitBreaker and onFallback (camel-resilience4j and camel-microprofile-fault-tolerance), a2aSubTask (camel-a2a), kamelet (camel-kamelet) and tokenizer (camel-langchain4j-tokenizer). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../component/a2a/A2ASubTaskBodyTypesTest.java | 94 ++++++++++++ .../tokenizer/TokenizerBodyTypesTest.java | 99 ++++++++++++ .../component/kamelet/KameletBodyTypesTest.java | 90 +++++++++++ .../FaultToleranceBodyTypesTest.java | 165 ++++++++++++++++++++ .../resilience4j/CircuitBreakerBodyTypesTest.java | 170 +++++++++++++++++++++ 5 files changed, 618 insertions(+) diff --git a/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2ASubTaskBodyTypesTest.java b/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2ASubTaskBodyTypesTest.java new file mode 100644 index 000000000000..de51d207b681 --- /dev/null +++ b/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2ASubTaskBodyTypesTest.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.a2a; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.A2ASubTaskDefinition; +import org.apache.camel.spi.BodyType.Carrier; +import org.apache.camel.spi.BodyTypes; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The body types of the a2aSubTask EIP (CAMEL-25928), checked at runtime: its steps start with the body as it was, and + * the body after it is what its steps end with. + */ +class A2ASubTaskBodyTypesTest extends CamelTestSupport { + + /** The body the steps end with. */ + private static final String END = "end of the steps"; + + private final List<Object> inside = new CopyOnWriteArrayList<>(); + + A2ASubTaskBodyTypesTest() { + testConfiguration().withUseRouteBuilder(false); + } + + @Test + void theStepsStartWithTheBodyAndTheBodyAfterIsWhatTheyEndWith() throws Exception { + BodyTypes bodyTypes = A2ASubTaskDefinition.class.getAnnotation(BodyTypes.class); + assertThat(bodyTypes).isNotNull(); + assertThat(bodyTypes.eipOut().from()).isEqualTo("steps"); + assertThat(bodyTypes.eipSteps().value()).containsExactly(Carrier.UNCHANGED); + + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:end") + .a2aSubTask() + .emitBefore("Before ${body}") + .emitAfter("After ${body}") + .process(e -> inside.add(e.getMessage().getBody())) + .setBody(constant(END)) + .end(); + + from("direct:keep") + .a2aSubTask() + .emitBefore("Before ${body}") + .emitAfter("After ${body}") + .process(e -> inside.add(e.getMessage().getBody())) + .end(); + } + }); + context.start(); + + Order body = new Order(); + Exchange out = template.send("direct:end", e -> e.getMessage().setBody(body)); + assertThat(out.getException()).isNull(); + assertThat(inside).hasSize(1); + assertThat(inside.get(0)).isSameAs(body); + assertThat(out.getMessage().getBody()).isEqualTo(END); + + // steps that leave the body as it was give the same instance after the EIP + inside.clear(); + out = template.send("direct:keep", e -> e.getMessage().setBody(body)); + assertThat(out.getException()).isNull(); + assertThat(inside).hasSize(1); + assertThat(inside.get(0)).isSameAs(body); + assertThat(out.getMessage().getBody()).isSameAs(body); + } + + /** A body that no converter knows: an EIP that keeps it gives the same instance. */ + private static final class Order { + } +} diff --git a/components/camel-ai/camel-langchain4j-tokenizer/src/test/java/org/apache/camel/component/langchain4j/tokenizer/TokenizerBodyTypesTest.java b/components/camel-ai/camel-langchain4j-tokenizer/src/test/java/org/apache/camel/component/langchain4j/tokenizer/TokenizerBodyTypesTest.java new file mode 100644 index 000000000000..239e87699aea --- /dev/null +++ b/components/camel-ai/camel-langchain4j-tokenizer/src/test/java/org/apache/camel/component/langchain4j/tokenizer/TokenizerBodyTypesTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.langchain4j.tokenizer; + +import java.nio.charset.StandardCharsets; + +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.TokenizerDefinition; +import org.apache.camel.model.tokenizer.LangChain4jTokenizerDefinition; +import org.apache.camel.spi.BodyType; +import org.apache.camel.spi.BodyType.Carrier; +import org.apache.camel.spi.BodyTypes; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * The body types of the tokenizer EIP (CAMEL-25928), checked at runtime: it reads the body as text and leaves the + * chunks of the text as a String[]. + */ +public class TokenizerBodyTypesTest extends LangChain4JTokenizerTestSupport { + + @Test + public void textGivesTheChunksAsAnArray() { + BodyTypes bodyTypes = TokenizerDefinition.class.getAnnotation(BodyTypes.class); + assertNotNull(bodyTypes); + assertEquals(Carrier.TEXT, bodyTypes.eipIn().value()[0]); + BodyType out = bodyTypes.eipOut(); + assertEquals(Carrier.LIST, out.value()[0]); + assertEquals("java.lang.String[]", out.javaType()); + + Exchange exchange = template.send("direct:start", e -> e.getMessage().setBody(TEXT)); + assertNull(exchange.getException()); + Object body = exchange.getMessage().getBody(); + assertInstanceOf(String[].class, body); + assertEquals(out.javaType(), body.getClass().getCanonicalName()); + assertEquals(4, ((String[]) body).length); + } + + @Test + public void bytesAreReadAsText() { + // the tokenizer reads the body with getBody(String.class), so bytes are converted to text first + Exchange exchange + = template.send("direct:start", e -> e.getMessage().setBody(TEXT.getBytes(StandardCharsets.UTF_8))); + assertNull(exchange.getException()); + Exchange text = template.send("direct:start", e -> e.getMessage().setBody(TEXT)); + assertArrayEquals((String[]) text.getMessage().getBody(), (String[]) exchange.getMessage().getBody()); + } + + @Test + public void anObjectNoConverterKnowsIsReadAsItsToString() { + // no converter knows Order, but the fallback converter to String uses toString(), so the object is not + // refused: its toString() is what is split into chunks + Order order = new Order(); + Exchange exchange = template.send("direct:start", e -> e.getMessage().setBody(order)); + assertNull(exchange.getException()); + assertArrayEquals(new String[] { order.toString() }, (String[]) exchange.getMessage().getBody()); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .tokenize(tokenizer() + .byParagraph() + .maxSegmentSize(1024) + .maxOverlap(10) + .using(LangChain4jTokenizerDefinition.TokenizerType.OPEN_AI) + .end()); + } + }; + } + + /** A body that no converter knows. */ + private static final class Order { + } +} diff --git a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletBodyTypesTest.java b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletBodyTypesTest.java new file mode 100644 index 000000000000..65d40d056e98 --- /dev/null +++ b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletBodyTypesTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.kamelet; + +import java.util.List; + +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.KameletDefinition; +import org.apache.camel.spi.BodyType; +import org.apache.camel.spi.BodyType.Carrier; +import org.apache.camel.spi.BodyTypes; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The body types of the kamelet EIP (CAMEL-25928), checked at runtime. The catalog says UNKNOWN for the body after it, + * as it is what the route of the Kamelet leaves: the same EIP gives a String with one Kamelet, keeps the body with + * another and gives a List with a third. + */ +public class KameletBodyTypesTest extends CamelTestSupport { + + @Test + public void theBodyAfterIsWhatTheKameletRouteLeaves() { + BodyTypes bodyTypes = KameletDefinition.class.getAnnotation(BodyTypes.class); + assertThat(bodyTypes).isNotNull(); + BodyType out = bodyTypes.eipOut(); + assertThat(out.value()).containsExactly(Carrier.UNKNOWN); + assertThat(out.from()).isEmpty(); + + Order body = new Order(); + + Exchange text = template.send("direct:text", e -> e.getMessage().setBody(body)); + assertThat(text.getException()).isNull(); + assertThat(text.getMessage().getBody()).isEqualTo("Hello from the Kamelet"); + + Exchange keep = template.send("direct:keep", e -> e.getMessage().setBody(body)); + assertThat(keep.getException()).isNull(); + assertThat(keep.getMessage().getBody()).isSameAs(body); + + Exchange list = template.send("direct:list", e -> e.getMessage().setBody(body)); + assertThat(list.getException()).isNull(); + assertThat(list.getMessage().getBody()).isEqualTo(List.of("a", "b")); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + routeTemplate("text") + .from("kamelet:source") + .setBody(constant("Hello from the Kamelet")); + + routeTemplate("keep") + .from("kamelet:source") + .setHeader("visited", constant(true)); + + routeTemplate("list") + .from("kamelet:source") + .setBody(constant(List.of("a", "b"))); + + from("direct:text").kamelet("text"); + from("direct:keep").kamelet("keep"); + from("direct:list").kamelet("list"); + } + }; + } + + /** A body that no converter knows: a Kamelet route that keeps it gives the same instance. */ + private static final class Order { + } +} diff --git a/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceBodyTypesTest.java b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceBodyTypesTest.java new file mode 100644 index 000000000000..50b8c53cee27 --- /dev/null +++ b/components/camel-microprofile/camel-microprofile-fault-tolerance/src/test/java/org/apache/camel/component/microprofile/faulttolerance/FaultToleranceBodyTypesTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.microprofile.faulttolerance; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.CircuitBreakerDefinition; +import org.apache.camel.model.OnFallbackDefinition; +import org.apache.camel.spi.BodyType.Carrier; +import org.apache.camel.spi.BodyTypes; +import org.apache.camel.spi.CircuitBreakerConstants; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The body types of the circuitBreaker and onFallback EIPs (CAMEL-25928), checked at runtime with MicroProfile Fault + * Tolerance: the steps start with the body the circuit breaker got, and the body after it is what its steps end with, + * or what onFallback ends with when a step failed or the circuit is open. The EIPs implemented in core are checked by + * EipBodyTypesTest in camel-core. + */ +public class FaultToleranceBodyTypesTest extends CamelTestSupport { + + /** The body the steps of circuitBreaker end with. */ + private static final String END = "end of the steps"; + /** The body the steps of onFallback end with. */ + private static final String FALLBACK_END = "end of onFallback"; + /** The body a step sets before the next step fails. */ + private static final String FAILED = "the body the failed step left"; + + private final List<Object> steps = new CopyOnWriteArrayList<>(); + private final List<Object> fallback = new CopyOnWriteArrayList<>(); + + @Test + public void theDeclarations() { + assertEquals("steps", bodyTypes(CircuitBreakerDefinition.class).eipOut().from()); + assertEquals(Carrier.UNCHANGED, bodyTypes(CircuitBreakerDefinition.class).eipSteps().value()[0]); + assertEquals("steps", bodyTypes(OnFallbackDefinition.class).eipOut().from()); + assertEquals(Carrier.UNCHANGED, bodyTypes(OnFallbackDefinition.class).eipSteps().value()[0]); + } + + @Test + public void theStepsStartWithTheBodyAndTheBodyAfterIsWhatTheyEndWith() { + Order body = new Order(); + Exchange out = send("direct:steps", body); + assertFalse(out.isFailed()); + assertEquals(END, out.getMessage().getBody()); + assertEquals(1, steps.size()); + assertSame(body, steps.get(0)); + assertTrue(fallback.isEmpty()); + } + + @Test + public void whenAStepFailsOnFallbackStartsWithTheBodyTheCircuitBreakerGot() { + Order body = new Order(); + Exchange out = send("direct:failed", body); + assertFalse(out.isFailed()); + assertEquals(FALLBACK_END, out.getMessage().getBody()); + assertEquals(1, steps.size()); + assertSame(body, steps.get(0)); + // the steps run on a copy of the exchange, so onFallback gets the body the circuit breaker got, + // not the body the failed step left + assertEquals(1, fallback.size()); + assertSame(body, fallback.get(0)); + assertEquals(false, out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED)); + } + + @Test + public void whenTheCircuitIsOpenOnFallbackStartsWithTheBodyTheCircuitBreakerGot() { + // two failures open the circuit, the third call is not attempted + send("direct:open", new Order()); + send("direct:open", new Order()); + + Order body = new Order(); + Exchange out = send("direct:open", body); + assertFalse(out.isFailed()); + assertEquals(FALLBACK_END, out.getMessage().getBody()); + assertTrue(steps.isEmpty()); + assertEquals(1, fallback.size()); + assertSame(body, fallback.get(0)); + assertEquals(true, out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED)); + assertInstanceOf(CircuitBreakerOpenException.class, out.getProperty(Exchange.EXCEPTION_CAUGHT)); + } + + private Exchange send(String uri, Object body) { + steps.clear(); + fallback.clear(); + return template.send(uri, e -> e.getMessage().setBody(body)); + } + + private void steps(Exchange exchange) { + steps.add(exchange.getMessage().getBody()); + } + + private void fallback(Exchange exchange) { + fallback.add(exchange.getMessage().getBody()); + } + + private static BodyTypes bodyTypes(Class<?> model) { + BodyTypes answer = model.getAnnotation(BodyTypes.class); + assertNotNull(answer, model.getSimpleName() + " has no @BodyTypes"); + return answer; + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:steps") + .circuitBreaker() + .process(e -> steps(e)).setBody(constant(END)) + .onFallback() + .process(e -> fallback(e)).setBody(constant(FALLBACK_END)) + .end(); + + from("direct:failed") + .circuitBreaker() + .process(e -> steps(e)).setBody(constant(FAILED)) + .throwException(new IllegalStateException("Forced")) + .onFallback() + .process(e -> fallback(e)).setBody(constant(FALLBACK_END)) + .end(); + + from("direct:open") + .circuitBreaker() + .faultToleranceConfiguration().requestVolumeThreshold(2).failureRatio(50).delay(60000).end() + .process(e -> steps(e)).setBody(constant(FAILED)) + .throwException(new IllegalStateException("Forced")) + .onFallback() + .process(e -> fallback(e)).setBody(constant(FALLBACK_END)) + .end(); + } + }; + } + + /** A body that no converter knows: a circuit breaker that keeps it gives the same instance. */ + private static final class Order { + } +} diff --git a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/CircuitBreakerBodyTypesTest.java b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/CircuitBreakerBodyTypesTest.java new file mode 100644 index 000000000000..0c3b0eb99972 --- /dev/null +++ b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/CircuitBreakerBodyTypesTest.java @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.component.resilience4j; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import org.apache.camel.BindToRegistry; +import org.apache.camel.Exchange; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.CircuitBreakerDefinition; +import org.apache.camel.model.OnFallbackDefinition; +import org.apache.camel.spi.BodyType.Carrier; +import org.apache.camel.spi.BodyTypes; +import org.apache.camel.spi.CircuitBreakerConstants; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The body types of the circuitBreaker and onFallback EIPs (CAMEL-25928), checked at runtime with Resilience4j: the + * steps start with the body the circuit breaker got, and the body after it is what its steps end with, or what + * onFallback ends with when a step failed or the circuit is open. The EIPs implemented in core are checked by + * EipBodyTypesTest in camel-core. + */ +public class CircuitBreakerBodyTypesTest extends CamelTestSupport { + + /** The body the steps of circuitBreaker end with. */ + private static final String END = "end of the steps"; + /** The body the steps of onFallback end with. */ + private static final String FALLBACK_END = "end of onFallback"; + /** The body a step sets before the next step fails. */ + private static final String FAILED = "the body the failed step left"; + + private final List<Object> steps = new CopyOnWriteArrayList<>(); + private final List<Object> fallback = new CopyOnWriteArrayList<>(); + + @BindToRegistry("openCircuitBreaker") + public CircuitBreaker openCircuitBreaker() { + return CircuitBreaker.ofDefaults("openCircuitBreaker"); + } + + @Test + public void theDeclarations() { + assertEquals("steps", bodyTypes(CircuitBreakerDefinition.class).eipOut().from()); + assertEquals(Carrier.UNCHANGED, bodyTypes(CircuitBreakerDefinition.class).eipSteps().value()[0]); + assertEquals("steps", bodyTypes(OnFallbackDefinition.class).eipOut().from()); + assertEquals(Carrier.UNCHANGED, bodyTypes(OnFallbackDefinition.class).eipSteps().value()[0]); + } + + @Test + public void theStepsStartWithTheBodyAndTheBodyAfterIsWhatTheyEndWith() { + Order body = new Order(); + Exchange out = send("direct:steps", body); + assertFalse(out.isFailed()); + assertEquals(END, out.getMessage().getBody()); + assertEquals(1, steps.size()); + assertSame(body, steps.get(0)); + assertTrue(fallback.isEmpty()); + } + + @Test + public void whenAStepFailsOnFallbackStartsWithTheBodyTheCircuitBreakerGot() { + Order body = new Order(); + Exchange out = send("direct:failed", body); + assertFalse(out.isFailed()); + assertEquals(FALLBACK_END, out.getMessage().getBody()); + assertEquals(1, steps.size()); + assertSame(body, steps.get(0)); + // the steps run on a copy of the exchange, so onFallback gets the body the circuit breaker got, + // not the body the failed step left + assertEquals(1, fallback.size()); + assertSame(body, fallback.get(0)); + assertEquals(false, out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED)); + } + + @Test + public void whenTheCircuitIsOpenOnFallbackStartsWithTheBodyTheCircuitBreakerGot() { + CircuitBreaker breaker = context.getRegistry().lookupByNameAndType("openCircuitBreaker", CircuitBreaker.class); + breaker.transitionToForcedOpenState(); + + Order body = new Order(); + Exchange out = send("direct:open", body); + assertFalse(out.isFailed()); + assertEquals(FALLBACK_END, out.getMessage().getBody()); + // the call is not attempted + assertTrue(steps.isEmpty()); + assertEquals(1, fallback.size()); + assertSame(body, fallback.get(0)); + assertEquals(true, out.getProperty(CircuitBreakerConstants.RESPONSE_REJECTED)); + assertInstanceOf(CallNotPermittedException.class, out.getProperty(Exchange.EXCEPTION_CAUGHT)); + } + + private Exchange send(String uri, Object body) { + steps.clear(); + fallback.clear(); + return template.send(uri, e -> e.getMessage().setBody(body)); + } + + private void steps(Exchange exchange) { + steps.add(exchange.getMessage().getBody()); + } + + private void fallback(Exchange exchange) { + fallback.add(exchange.getMessage().getBody()); + } + + private static BodyTypes bodyTypes(Class<?> model) { + BodyTypes answer = model.getAnnotation(BodyTypes.class); + assertNotNull(answer, model.getSimpleName() + " has no @BodyTypes"); + return answer; + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:steps") + .circuitBreaker() + .process(e -> steps(e)).setBody(constant(END)) + .onFallback() + .process(e -> fallback(e)).setBody(constant(FALLBACK_END)) + .end(); + + from("direct:failed") + .circuitBreaker() + .process(e -> steps(e)).setBody(constant(FAILED)) + .throwException(new IllegalStateException("Forced")) + .onFallback() + .process(e -> fallback(e)).setBody(constant(FALLBACK_END)) + .end(); + + from("direct:open") + .circuitBreaker().resilience4jConfiguration().circuitBreaker("openCircuitBreaker").end() + .process(e -> steps(e)).setBody(constant(END)) + .onFallback() + .process(e -> fallback(e)).setBody(constant(FALLBACK_END)) + .end(); + } + }; + } + + /** A body that no converter knows: a circuit breaker that keeps it gives the same instance. */ + private static final class Order { + } +}
