This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new d747bffa5745 CAMEL-25069: camel-core - Rest DSL: fix the follow-ups
from the deep review (#27078)
d747bffa5745 is described below
commit d747bffa574504ae2ac895d15ced051b08037173
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 30 12:42:47 2026 +0200
CAMEL-25069: camel-core - Rest DSL: fix the follow-ups from the deep review
(#27078)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../java/org/apache/camel/impl/DefaultModel.java | 151 +++++++++++---------
.../apache/camel/model/RouteDefinitionHelper.java | 11 +-
.../camel/model/rest/RestBindingDefinition.java | 25 +++-
.../apache/camel/model/rest/RestDefinition.java | 42 ++++--
.../camel/reifier/rest/RestBindingReifier.java | 9 ++
.../camel/component/rest/RestDslEdgeCasesTest.java | 152 +++++++++++++++++++++
.../camel/support/processor/RestBindingAdvice.java | 61 +++++++--
.../processor/RestBindingAdviceFactory.java | 4 +-
.../processor/RestBindingConfiguration.java | 12 ++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 19 +++
docs/user-manual/modules/ROOT/pages/rest-dsl.adoc | 2 +
11 files changed, 396 insertions(+), 92 deletions(-)
diff --git
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
index 49238b5d0ebd..48107f9cb2f4 100644
---
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
+++
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
@@ -225,81 +225,74 @@ public class DefaultModel implements Model {
}
}
}
+ // how many rest services call each direct route
+ Map<String, Integer> directUsage = new HashMap<>();
+ for (RouteDefinition r : allRoutes) {
+ ToDefinition to = restRouteToInline(r);
+ if (to != null) {
+ directUsage.merge(to.getEndpointUri(), 1,
Integer::sum);
+ }
+ }
for (RouteDefinition r : allRoutes) {
// loop all rest routes
FromDefinition from = r.getInput();
- if (from != null && !r.isInlined()) {
- // only attempt to inline if not already inlined
- String uri = from.getEndpointUri();
- if (uri != null && uri.startsWith("rest:")) {
- // find first EIP in the outputs (skip abstract
which are onException/intercept
- // etc)
- ToDefinition to = null;
- for (ProcessorDefinition<?> def : r.getOutputs()) {
- if (def.isAbstract()) {
- continue;
+ ToDefinition to = restRouteToInline(r);
+ if (to != null) {
+ String toUri = to.getEndpointUri();
+ RouteDefinition toBeInlined = directs.get(toUri);
+ // a route used by more than one rest service is not
inlined (its outputs can only
+ // belong to one route), so the rest services call it
+ if (toBeInlined != null &&
directUsage.getOrDefault(toUri, 0) == 1) {
+ toBeRemoved.add(toBeInlined);
+ // inline the source loc:line as starting from
this direct input
+ FromDefinition inlinedFrom =
toBeInlined.getInput();
+ from.setLocation(inlinedFrom.getLocation());
+ from.setLineNumber(inlinedFrom.getLineNumber());
+ // inline by replacing the outputs (preserve all
abstracts such as interceptors)
+ List<ProcessorDefinition<?>> toBeRemovedOut = new
ArrayList<>();
+ for (ProcessorDefinition<?> out : r.getOutputs()) {
+ // should be removed if to be added via inlined
+ boolean remove =
toBeInlined.getOutputs().stream().anyMatch(o -> o == out);
+ if (!remove) {
+ remove = !out.isAbstract(); // remove all
non abstract
}
- if (def instanceof ToDefinition toDefinition) {
- to = toDefinition;
+ if (remove) {
+ toBeRemovedOut.add(out);
}
- break;
}
- if (to != null) {
- String toUri = to.getEndpointUri();
- RouteDefinition toBeInlined =
directs.get(toUri);
- if (toBeInlined != null) {
- toBeRemoved.add(toBeInlined);
- // inline the source loc:line as starting
from this direct input
- FromDefinition inlinedFrom =
toBeInlined.getInput();
-
from.setLocation(inlinedFrom.getLocation());
-
from.setLineNumber(inlinedFrom.getLineNumber());
- // inline by replacing the outputs
(preserve all abstracts such as interceptors)
- List<ProcessorDefinition<?>>
toBeRemovedOut = new ArrayList<>();
- for (ProcessorDefinition<?> out :
r.getOutputs()) {
- // should be removed if to be added
via inlined
- boolean remove =
toBeInlined.getOutputs().stream().anyMatch(o -> o == out);
- if (!remove) {
- remove = !out.isAbstract(); //
remove all non abstract
- }
- if (remove) {
- toBeRemovedOut.add(out);
- }
- }
- r.getOutputs().removeAll(toBeRemovedOut);
-
r.getOutputs().addAll(toBeInlined.getOutputs());
- // inlined outputs should have re-assigned
parent to this route
- r.getOutputs().forEach(o ->
o.setParent(r));
- // and copy over various configurations
- if (toBeInlined.getRouteId() != null) {
- r.setId(toBeInlined.getRouteId());
- }
-
r.setNodePrefixId(toBeInlined.getNodePrefixId());
- r.setGroup(toBeInlined.getGroup());
-
r.setAutoStartup(toBeInlined.getAutoStartup());
- r.setDelayer(toBeInlined.getDelayer());
- r.setInputType(toBeInlined.getInputType());
-
r.setOutputType(toBeInlined.getOutputType());
- r.setLogMask(toBeInlined.getLogMask());
-
r.setMessageHistory(toBeInlined.getMessageHistory());
- if (toBeInlined.getStreamCache() != null) {
- // keep stream caching from the rest
verb unless the inlined route sets it
-
r.setStreamCache(toBeInlined.getStreamCache());
- }
- r.setTrace(toBeInlined.getTrace());
-
r.setStartupOrder(toBeInlined.getStartupOrder());
-
r.setRoutePolicyRef(toBeInlined.getRoutePolicyRef());
-
r.setRouteConfigurationId(toBeInlined.getRouteConfigurationId());
-
r.setRoutePolicies(toBeInlined.getRoutePolicies());
-
r.setShutdownRoute(toBeInlined.getShutdownRoute());
-
r.setShutdownRunningTask(toBeInlined.getShutdownRunningTask());
-
r.setErrorHandlerRef(toBeInlined.getErrorHandlerRef());
-
r.setPrecondition(toBeInlined.getPrecondition());
- if
(toBeInlined.isErrorHandlerFactorySet()) {
-
r.setErrorHandler(toBeInlined.getErrorHandler());
- }
- r.markInlined();
- }
+ r.getOutputs().removeAll(toBeRemovedOut);
+ r.getOutputs().addAll(toBeInlined.getOutputs());
+ // inlined outputs should have re-assigned parent
to this route
+ r.getOutputs().forEach(o -> o.setParent(r));
+ // and copy over various configurations
+ if (toBeInlined.getRouteId() != null) {
+ r.setId(toBeInlined.getRouteId());
}
+ r.setNodePrefixId(toBeInlined.getNodePrefixId());
+ r.setGroup(toBeInlined.getGroup());
+ r.setAutoStartup(toBeInlined.getAutoStartup());
+ r.setDelayer(toBeInlined.getDelayer());
+ r.setInputType(toBeInlined.getInputType());
+ r.setOutputType(toBeInlined.getOutputType());
+ r.setLogMask(toBeInlined.getLogMask());
+
r.setMessageHistory(toBeInlined.getMessageHistory());
+ if (toBeInlined.getStreamCache() != null) {
+ // keep stream caching from the rest verb
unless the inlined route sets it
+ r.setStreamCache(toBeInlined.getStreamCache());
+ }
+ r.setTrace(toBeInlined.getTrace());
+ r.setStartupOrder(toBeInlined.getStartupOrder());
+
r.setRoutePolicyRef(toBeInlined.getRoutePolicyRef());
+
r.setRouteConfigurationId(toBeInlined.getRouteConfigurationId());
+ r.setRoutePolicies(toBeInlined.getRoutePolicies());
+ r.setShutdownRoute(toBeInlined.getShutdownRoute());
+
r.setShutdownRunningTask(toBeInlined.getShutdownRunningTask());
+
r.setErrorHandlerRef(toBeInlined.getErrorHandlerRef());
+ r.setPrecondition(toBeInlined.getPrecondition());
+ if (toBeInlined.isErrorHandlerFactorySet()) {
+
r.setErrorHandler(toBeInlined.getErrorHandler());
+ }
+ r.markInlined();
}
}
}
@@ -323,6 +316,28 @@ public class DefaultModel implements Model {
}
}
+ /**
+ * The to of a rest route (not already inlined) that is a candidate for
inlining, which is the first EIP in the
+ * outputs (skipping abstracts such as onException and intercept)
+ */
+ private static ToDefinition restRouteToInline(RouteDefinition r) {
+ FromDefinition from = r.getInput();
+ if (from == null || r.isInlined()) {
+ return null;
+ }
+ String uri = from.getEndpointUri();
+ if (uri == null || !uri.startsWith("rest:")) {
+ return null;
+ }
+ for (ProcessorDefinition<?> def : r.getOutputs()) {
+ if (def.isAbstract()) {
+ continue;
+ }
+ return def instanceof ToDefinition toDefinition ? toDefinition :
null;
+ }
+ return null;
+ }
+
@Override
public void addRouteDefinition(RouteDefinition routeDefinition) throws
Exception {
addRouteDefinitions(Collections.singletonList(routeDefinition));
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
index df38a7ac03c4..658af1592e8d 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteDefinitionHelper.java
@@ -139,6 +139,15 @@ public final class RouteDefinitionHelper {
// generated ids
Set<String> customIds = new HashSet<>();
+ // the custom ids of the routes that are not generated from a rest
verb: a verb with the same id as such a route
+ // gets a generated id instead (a duplicate id between verbs is kept,
so it is reported as a duplicate route id)
+ Set<String> routeIds = new HashSet<>();
+ for (final RouteDefinition route : routes) {
+ if (route.hasCustomIdAssigned()) {
+
routeIds.add(context.resolvePropertyPlaceholders(route.getId()));
+ }
+ }
+
for (final RouteDefinition route : routes) {
// if there was a custom id assigned, then make sure to support
// property placeholders
@@ -157,7 +166,7 @@ public final class RouteDefinitionHelper {
VerbDefinition verb = findVerbDefinition(context, rest,
route.getInput().getEndpointUri());
if (verb != null) {
String id =
context.resolvePropertyPlaceholders(verb.getId());
- if (verb.hasCustomIdAssigned() &&
ObjectHelper.isNotEmpty(id) && !customIds.contains(id)) {
+ if (verb.hasCustomIdAssigned() &&
ObjectHelper.isNotEmpty(id) && !routeIds.contains(id)) {
route.setId(id);
customIds.add(id);
}
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java
index 8b17d04cf5ae..b50b51f472d9 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestBindingDefinition.java
@@ -53,6 +53,8 @@ public class RestBindingDefinition extends
OptionalIdentifiedDefinition<RestBind
private Map<String, String> responseCodes;
@XmlTransient
private Set<String> responseHeaders;
+ @XmlTransient
+ private Map<String, Set<String>> responseCodeHeaders;
@XmlAttribute
@Metadata(description = "The content type the REST service accepts
(consumes) as input, such as application/xml or application/json.")
@@ -170,7 +172,7 @@ public class RestBindingDefinition extends
OptionalIdentifiedDefinition<RestBind
}
/**
- * Adds a response code
+ * Adds a response header that is required on every response
*/
public void addResponseHeader(String headerName) {
// content-type header should be skipped
@@ -187,6 +189,27 @@ public class RestBindingDefinition extends
OptionalIdentifiedDefinition<RestBind
return responseHeaders;
}
+ /**
+ * Adds a response header that is required on the responses of the given
response code
+ */
+ public void addResponseHeader(String code, String headerName) {
+ // content-type header should be skipped
+ if ("content-type".equalsIgnoreCase(headerName)) {
+ return;
+ }
+ if (responseCodeHeaders == null) {
+ responseCodeHeaders = new HashMap<>();
+ }
+ responseCodeHeaders.computeIfAbsent(code, k -> new
HashSet<>()).add(headerName);
+ }
+
+ /**
+ * The response headers that are required, per response code
+ */
+ public Map<String, Set<String>> getResponseCodeHeaders() {
+ return responseCodeHeaders;
+ }
+
/**
* Adds a required HTTP header
*
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
index 0537f34a160b..5661149a7f9f 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
@@ -1198,6 +1198,20 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
} else {
binding.setEnableNoContentResponse(getEnableNoContentResponse());
}
+ // the body parameter from the type must be added before the
parameters are registered on the binding,
+ // so a required body is enforced
+ if (verb.getType() != null) {
+ String bodyType = parseText(camelContext, verb.getType());
+ ParamDefinition param = findBodyParam(verb);
+ if (param == null) {
+ // must be body type and set the model class as data type
+
param(verb).name(RestParamType.body.name()).type(RestParamType.body).dataType(bodyType).endParam();
+ } else {
+ // must be body type and set the model class as data type
+ param.type(RestParamType.body).dataType(bodyType);
+ }
+ }
+
for (ParamDefinition param : verb.getParams()) {
// register all the default values for the query and header
parameters
RestParamType type = param.getType();
@@ -1228,7 +1242,8 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
if (rm.getHeaders() != null) {
for (var header : rm.getHeaders()) {
String name = parseText(camelContext,
header.getName());
- binding.addResponseHeader(name);
+ // the headers are only required on the responses of
this code
+ binding.addResponseHeader(rm.getCode(), name);
}
}
}
@@ -1294,18 +1309,6 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
uriTemplating(camelContext, verb, allPath, false);
}
- if (verb.getType() != null) {
- String bodyType = parseText(camelContext, verb.getType());
- ParamDefinition param = findParam(verb,
RestParamType.body.name());
- if (param == null) {
- // must be body type and set the model class as data type
-
param(verb).name(RestParamType.body.name()).type(RestParamType.body).dataType(bodyType).endParam();
- } else {
- // must be body type and set the model class as data type
- param.type(RestParamType.body).dataType(bodyType);
- }
- }
-
// create the from endpoint uri which is using the rest component
String from = buildFromUri(camelContext, verb);
@@ -1416,6 +1419,19 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
return parseText(camelContext, answer);
}
+ /**
+ * The body parameter of the verb, which is the parameter of type body
(whatever its name), or else the parameter
+ * named body
+ */
+ private ParamDefinition findBodyParam(VerbDefinition verb) {
+ for (ParamDefinition param : verb.getParams()) {
+ if (param.getType() == RestParamType.body) {
+ return param;
+ }
+ }
+ return findParam(verb, RestParamType.body.name());
+ }
+
private ParamDefinition findParam(VerbDefinition verb, String name) {
for (ParamDefinition param : verb.getParams()) {
if (name.equals(param.getName())) {
diff --git
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/rest/RestBindingReifier.java
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/rest/RestBindingReifier.java
index 6fbe8c8df71c..0df14c8af849 100644
---
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/rest/RestBindingReifier.java
+++
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/rest/RestBindingReifier.java
@@ -16,6 +16,10 @@
*/
package org.apache.camel.reifier.rest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
import org.apache.camel.Route;
import org.apache.camel.model.rest.RestBindingDefinition;
import org.apache.camel.model.rest.RestBindingMode;
@@ -85,6 +89,11 @@ public class RestBindingReifier extends AbstractReifier {
rbc.setOutTypeClass(definition.getOutTypeClass());
rbc.setResponseCodes(parseMap(definition.getResponseCodes()));
rbc.setResponseHeaders(parseSet(definition.getResponseHeaders()));
+ if (definition.getResponseCodeHeaders() != null) {
+ Map<String, Set<String>> headers = new HashMap<>();
+ definition.getResponseCodeHeaders().forEach((k, v) ->
headers.put(k, parseSet(v)));
+ rbc.setResponseCodeHeaders(headers);
+ }
// use factory to create advice
return RestBindingAdviceFactory.build(camelContext, rbc);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/component/rest/RestDslEdgeCasesTest.java
b/core/camel-core/src/test/java/org/apache/camel/component/rest/RestDslEdgeCasesTest.java
index bb3f44495867..e6e6b3557b92 100644
---
a/core/camel-core/src/test/java/org/apache/camel/component/rest/RestDslEdgeCasesTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/component/rest/RestDslEdgeCasesTest.java
@@ -23,10 +23,12 @@ import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.model.rest.RestBindingMode;
+import org.apache.camel.model.rest.RestParamType;
import org.apache.camel.spi.Registry;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class RestDslEdgeCasesTest extends ContextTestSupport {
@@ -128,4 +130,154 @@ public class RestDslEdgeCasesTest extends
ContextTestSupport {
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
assertMockEndpointsSatisfied();
}
+
+ @Test
+ public void testClientResponseValidationWithBindingOff() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
restConfiguration().host("localhost").clientResponseValidation(true);
+ rest("/h").get()
+
.responseMessage().code(200).header("X-Id").endHeader().endResponseMessage()
+ .to("direct:h");
+
+ from("direct:h").setBody(constant("Hello"));
+ }
+ });
+ context.start();
+
+ // the response is validated also when binding is off
+ Exchange out = template.request("seda:get-h", e -> {
+ });
+
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(500);
+ assertThat(out.getMessage().getBody(String.class)).isEqualTo("Some of
the response HTTP headers are missing.");
+ }
+
+ @Test
+ public void testClientResponseValidationHeadersPerResponseCode() throws
Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
restConfiguration().host("localhost").clientResponseValidation(true);
+ rest("/c").get()
+
.responseMessage().code(200).header("X-Id").endHeader().endResponseMessage()
+ .responseMessage().code(404).message("Not
found").endResponseMessage()
+ .to("direct:c");
+
+ from("direct:c")
+ .choice()
+ .when(header("found"))
+ .setHeader("X-Id",
constant("123")).setBody(constant("Found"))
+ .otherwise()
+ .setHeader(Exchange.HTTP_RESPONSE_CODE,
constant(404)).setBody(constant("Not found"));
+ }
+ });
+ context.start();
+
+ // X-Id is only required on the 200 response
+ Exchange out = template.request("seda:get-c", e -> {
+ });
+
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(404);
+ assertThat(out.getMessage().getBody(String.class)).isEqualTo("Not
found");
+
+ out = template.request("seda:get-c", e ->
e.getMessage().setHeader("found", true));
+
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+ assertThat(out.getMessage().getBody(String.class)).isEqualTo("Found");
+ }
+
+ @Test
+ public void testTypeRequiresBody() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
restConfiguration().host("localhost").clientRequestValidation(true);
+ rest("/t").post().type(String.class).to("mock:t");
+ }
+ });
+ context.start();
+
+ // the body parameter from type(...) is required (by default), so a
missing body is a bad request
+ getMockEndpoint("mock:t").expectedMessageCount(0);
+ Exchange out = template.request("seda:post-t", e ->
e.getMessage().setBody(null));
+
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+ assertThat(out.getMessage().getBody(String.class)).isEqualTo("The
request body is missing.");
+ assertMockEndpointsSatisfied();
+ }
+
+ @Test
+ public void testTypeWithOptionalBodyParamOfAnotherName() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
restConfiguration().host("localhost").clientRequestValidation(true);
+ rest("/o").post().type(String.class)
+
.param().name("payload").type(RestParamType.body).required(false).endParam()
+ .to("mock:o");
+ }
+ });
+ context.start();
+
+ // the body parameter (whatever its name) is not required, so a
missing body is accepted
+ getMockEndpoint("mock:o").expectedMessageCount(1);
+ Exchange out = template.request("seda:post-o", e ->
e.getMessage().setBody(null));
+
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+ assertMockEndpointsSatisfied();
+ }
+
+ @Test
+ public void testVerbIdSameAsRouteId() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().host("localhost").inlineRoutes(false);
+ rest("/v").get().id("hello").to("direct:hello");
+
from("direct:hello").routeId("hello").transform().constant("Hello");
+ }
+ });
+ context.start();
+
+ // the route keeps its id, and the verb gets a generated id
+
assertThat(context.getRoute("hello").getEndpoint().getEndpointUri()).isEqualTo("direct://hello");
+ assertThat(context.getRoutes()).hasSize(2);
+ }
+
+ @Test
+ public void testDuplicateVerbId() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().host("localhost");
+ rest("/d")
+ .get("/a").id("dup").to("mock:a")
+ .get("/b").id("dup").to("mock:b");
+ }
+ });
+
+ // the duplicate id is reported (and not replaced by a generated id)
+ assertThatThrownBy(() ->
context.start()).hasStackTraceContaining("Duplicate id detected: dup");
+ }
+
+ @Test
+ public void testSameDirectUsedByTwoRests() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().host("localhost").inlineRoutes(true);
+ rest("/a").get().to("direct:x");
+ rest("/b").get().to("direct:x");
+
+ from("direct:x").routeId("x").to("mock:x");
+ }
+ });
+ context.start();
+
+ // the direct route is used by two rest services, so it is not inlined
(into both of them)
+ assertThat(context.getRouteDefinition("x")).isNotNull();
+ assertThat(context.getRouteDefinitions()).hasSize(3);
+
+ getMockEndpoint("mock:x").expectedBodiesReceivedInAnyOrder("A", "B");
+ template.sendBody("seda:get-a", "A");
+ template.sendBody("seda:get-b", "B");
+ assertMockEndpointsSatisfied();
+ }
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
index 3edc8138a30f..7371f6832412 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdvice.java
@@ -17,6 +17,7 @@
package org.apache.camel.support.processor;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -82,6 +83,7 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
private final Set<String> requiredHeaders;
private final Map<String, String> responseCodes;
private final Set<String> responseHeaders;
+ private Map<String, Set<String>> responseCodeHeaders;
/**
* Use {@link RestBindingAdviceFactory} to create.
@@ -338,6 +340,18 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
}
private void marshal(Exchange exchange, Map<String, Object> state) {
+ doMarshal(exchange, state);
+
+ // perform client response validation (also when the response body was
not marshalled, such as when binding
+ // is off, the body is empty or binding was skipped for an error code)
+ RestClientResponseValidator.ValidationError error =
doClientResponseValidation(exchange);
+ if (error != null) {
+ exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
error.statusCode());
+ exchange.getMessage().setBody(error.body());
+ }
+ }
+
+ private void doMarshal(Exchange exchange, Map<String, Object> state) {
// only marshal if there was no exception
if (exchange.getException() != null) {
return;
@@ -467,13 +481,6 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
exchange, e.getMessage(), e);
exchange.setException(e);
}
-
- // perform client response validation
- RestClientResponseValidator.ValidationError error =
doClientResponseValidation(exchange);
- if (error != null) {
- exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE,
error.statusCode());
- exchange.getMessage().setBody(error.body());
- }
}
/**
@@ -607,12 +614,50 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
public RestClientResponseValidator.ValidationError
doClientResponseValidation(Exchange exchange) {
if (clientResponseValidation && clientResponseValidator != null &&
!exchange.isFailed()) {
RestClientResponseValidator.ValidationContext vc = new
RestClientResponseValidator.ValidationContext(
- consumes, produces, responseCodes, responseHeaders);
+ consumes, produces, responseCodes,
resolveResponseHeaders(exchange));
return clientResponseValidator.validate(exchange, vc);
}
return null;
}
+ /**
+ * The response headers that are required on the response, which are the
headers of the response message of the
+ * response code (or the default response message when the code has none)
+ */
+ private Set<String> resolveResponseHeaders(Exchange exchange) {
+ if (responseCodeHeaders == null) {
+ return responseHeaders;
+ }
+ String code =
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, "200",
String.class);
+ Set<String> headers = responseCodeHeaders.get(code);
+ if (headers == null && (responseCodes == null ||
!responseCodes.containsKey(code))) {
+ headers = responseCodeHeaders.get("default");
+ }
+ if (responseHeaders == null || responseHeaders.isEmpty()) {
+ return headers;
+ }
+ if (headers == null) {
+ return responseHeaders;
+ }
+ Set<String> answer = new HashSet<>(responseHeaders);
+ answer.addAll(headers);
+ return answer;
+ }
+
+ /**
+ * The response headers that are required, per response code
+ */
+ public Map<String, Set<String>> getResponseCodeHeaders() {
+ return responseCodeHeaders;
+ }
+
+ /**
+ * Sets the response headers that are required, per response code
+ */
+ public void setResponseCodeHeaders(Map<String, Set<String>>
responseCodeHeaders) {
+ this.responseCodeHeaders = responseCodeHeaders;
+ }
+
@Override
protected void doStart() throws Exception {
ServiceHelper.startService(jsonUnmarshal, xmlUnmarshal, jsonMarshal,
xmlMarshal);
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
index 70977c46108c..cd6d58c6fe21 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingAdviceFactory.java
@@ -135,12 +135,14 @@ public class RestBindingAdviceFactory {
responseValidator =
lookupRestClientResponseValidator(camelContext);
}
- return new RestBindingAdvice(
+ RestBindingAdvice advice = new RestBindingAdvice(
camelContext, json, xml, outJson, outXml,
bc.getConsumes(), bc.getProduces(), mode,
bc.isSkipBindingOnErrorCode(), bc.isClientRequestValidation(),
bc.isClientResponseValidation(), bc.isEnableCORS(),
bc.isEnableNoContentResponse(), bc.getCorsHeaders(),
bc.getQueryDefaultValues(), bc.getQueryAllowedValues(),
bc.isRequiredBody(), bc.getRequiredQueryParameters(),
bc.getRequiredHeaders(), bc.getResponseCodes(),
bc.getResponseHeaders(), requestValidator, responseValidator);
+ advice.setResponseCodeHeaders(bc.getResponseCodeHeaders());
+ return advice;
}
protected static void setupJson(
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingConfiguration.java
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingConfiguration.java
index 4a9b55ab5eee..a7ed7f30907f 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingConfiguration.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/processor/RestBindingConfiguration.java
@@ -45,6 +45,7 @@ public class RestBindingConfiguration {
private Class<?> outTypeClass;
private Map<String, String> responseCodes;
private Set<String> responseHeaders;
+ private Map<String, Set<String>> responseCodeHeaders;
public String getConsumes() {
return consumes;
@@ -213,4 +214,15 @@ public class RestBindingConfiguration {
public void setResponseHeaders(Set<String> responseHeaders) {
this.responseHeaders = responseHeaders;
}
+
+ /**
+ * The response headers that are required, per response code
+ */
+ public Map<String, Set<String>> getResponseCodeHeaders() {
+ return responseCodeHeaders;
+ }
+
+ public void setResponseCodeHeaders(Map<String, Set<String>>
responseCodeHeaders) {
+ this.responseCodeHeaders = responseCodeHeaders;
+ }
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index a9d0ab1df4f3..3b55ad81d6ef 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -375,6 +375,25 @@ mode is only set with `restConfiguration()`, as it already
did when it is set on
With `clientRequestValidation` enabled, such a verb now rejects a request with
another `Content-Type` (415) or
`Accept` (406) header.
+=== Rest DSL - client validation, verb ids and inlined routes
+
+- `clientResponseValidation` now also validates the response when the binding
mode is `off`, the response has no
+ body, or binding was skipped because of an error code
(`skipBindingOnErrorCode`). Before, the response was only
+ validated after the body was marshalled.
+- The headers of a `responseMessage` are only required on the responses with
that code (or on the responses of the
+ `default` response message when the code has none). Before, the headers of
all the response messages were required
+ on every response.
+- A verb with `type` gets a body parameter that is required (unless the body
parameter, whatever its name, is
+ declared with `required=false`).
+ With `clientRequestValidation` enabled, a request without a body is now
rejected (400), as the body was already
+ marked as required in the OpenAPI specification.
+- Two verbs with the same `id` now fail with a duplicate route id error.
Before, the second verb silently got a
+ generated route id. A verb with the same `id` as a route that is not a rest
verb (such as the `direct` route it
+ calls) still gets a generated route id, as before.
+- A `direct` route that more than one rest service calls is no longer inlined
(with `inlineRoutes` enabled, which is
+ the default). It stays a route of its own, and the rest services call it.
Before, its outputs were inlined into all
+ the rest services, which then shared the same EIP definitions.
+
=== CamelEvent JSON serialization
`CamelEvent` now provides `asJSon()` and `toJSon(int indent)` with default
implementations that
diff --git a/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
b/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
index c2ee39f773d2..cf037a63c6aa 100644
--- a/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
+++ b/docs/user-manual/modules/ROOT/pages/rest-dsl.adoc
@@ -325,6 +325,8 @@ WARNING: When using inline-routes, then each REST endpoint
should link 1:1 to a
The linked _direct_ routes are inlined and therefore does not **exists** as
independent routes, and
they cannot be called from other regular Camel routes. In other words the
inlined routes are essentially
moved inside the rest-dsl and does not exist as a route. See more detils
further below.
+A `direct` route that is linked from more than one REST endpoint is not
inlined, but stays a route of its own
+that the REST endpoints call.
To do this you *MUST* use `direct` endpoints, and each endpoint must be unique
name per service.
And the option `inlineRoutes` must be enabled.