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 7b90c04b60ab CAMEL-25046: camel-core - Rest DSL: fix bugs found in a
deep review (#26927)
7b90c04b60ab is described below
commit 7b90c04b60abeddf6b27386f0ffb168a7755ee43
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 09:09:23 2026 +0200
CAMEL-25046: camel-core - Rest DSL: fix bugs found in a deep review (#26927)
- responseMessage(int, String) adds the message to the verb
- an Accept/Content-Type with parameters (q, charset) on any media type is
matched by its media type
- consumes/produces are inferred from the binding mode of the rest
configuration too
- an inlined route keeps the stream caching of the rest verb unless it sets
its own
- bindingMode("{{placeholder}}") keeps the case of the placeholder
- enableNoContentResponse detects <el></el> without an xml declaration
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../java/org/apache/camel/impl/DefaultModel.java | 5 +-
.../apache/camel/model/rest/RestDefinition.java | 19 ++-
.../camel/component/rest/RestDslEdgeCasesTest.java | 131 +++++++++++++++++++++
.../apache/camel/support/http/RestUtilTest.java | 11 ++
.../processor/RestBindingAdviceEmptyXmlTest.java | 43 +++++++
.../org/apache/camel/support/http/RestUtil.java | 14 +--
.../camel/support/processor/RestBindingAdvice.java | 42 ++++---
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 8 ++
8 files changed, 244 insertions(+), 29 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 1fbf9a703805..b205ebb6db4c 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
@@ -280,7 +280,10 @@ public class DefaultModel implements Model {
r.setOutputType(toBeInlined.getOutputType());
r.setLogMask(toBeInlined.getLogMask());
r.setMessageHistory(toBeInlined.getMessageHistory());
-
r.setStreamCache(toBeInlined.getStreamCache());
+ 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());
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 5b3163f48d20..0537f34a160b 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
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
@@ -45,6 +46,7 @@ import
org.apache.camel.model.errorhandler.NoErrorHandlerDefinition;
import org.apache.camel.spi.AsEndpointUri;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.NodeIdFactory;
+import org.apache.camel.spi.PropertiesComponent;
import org.apache.camel.spi.Resource;
import org.apache.camel.spi.ResourceAware;
import org.apache.camel.spi.RestConfiguration;
@@ -531,6 +533,7 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
ResponseMessageDefinition msg = responseMessage(verb);
msg.setCode(String.valueOf(code));
msg.setMessage(message);
+ verb.getResponseMsgs().add(msg);
return this;
}
@@ -619,12 +622,16 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
}
public RestDefinition bindingMode(String mode) {
+ // a property placeholder must keep its case (the key is
case-sensitive)
+ if (!mode.contains(PropertiesComponent.PREFIX_TOKEN)) {
+ mode = mode.toLowerCase(Locale.ENGLISH);
+ }
if (getVerbs().isEmpty()) {
- this.bindingMode = mode.toLowerCase();
+ this.bindingMode = mode;
} else {
// add on last verb as that is how the Java DSL works
VerbDefinition verb = getVerbs().get(getVerbs().size() - 1);
- verb.setBindingMode(mode.toLowerCase());
+ verb.setBindingMode(mode);
}
return this;
@@ -1136,9 +1143,13 @@ public class RestDefinition extends
OptionalIdentifiedDefinition<RestDefinition>
} else {
binding.setProduces(getProduces());
}
- if (binding.getType() != null || binding.getOutType() != null &&
binding.getBindingMode() != null) {
+ // the binding mode from the verb or rest, or else from the rest
configuration
+ String mode = parseText(camelContext, binding.getBindingMode());
+ if (mode == null &&
camelContext.getRestConfiguration().getBindingMode() != null) {
+ mode =
camelContext.getRestConfiguration().getBindingMode().name();
+ }
+ if (mode != null && (binding.getType() != null ||
binding.getOutType() != null)) {
// okay we have binding mode and in/out type defined - then we
can infer consume/produces
- String mode = binding.getBindingMode();
if ("json".equals(mode)) {
if (binding.getConsumes() == null && binding.getType() !=
null) {
binding.setConsumes("application/json");
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
new file mode 100644
index 000000000000..bb3f44495867
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/component/rest/RestDslEdgeCasesTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.rest;
+
+import java.util.Properties;
+
+import org.apache.camel.ContextTestSupport;
+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.spi.Registry;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RestDslEdgeCasesTest extends ContextTestSupport {
+
+ @Override
+ protected Registry createCamelRegistry() throws Exception {
+ Registry registry = super.createCamelRegistry();
+ registry.bind("dummy-rest", new DummyRestConsumerFactory());
+ return registry;
+ }
+
+ @Override
+ public boolean isUseRouteBuilder() {
+ return false;
+ }
+
+ private RouteDefinition restRoute() {
+ return context.getRouteDefinitions().stream().filter(r ->
Boolean.TRUE.equals(r.isRest())).findFirst()
+ .orElseThrow();
+ }
+
+ @Test
+ public void testResponseMessageWithIntCode() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().host("localhost");
+ rest("/r").get().responseMessage(404, "Not
found").to("mock:r");
+ }
+ });
+
assertThat(context.getRestDefinitions().get(0).getVerbs().get(0).getResponseMsgs())
+ .singleElement().satisfies(m -> {
+ assertThat(m.getCode()).isEqualTo("404");
+ assertThat(m.getMessage()).isEqualTo("Not found");
+ });
+ }
+
+ @Test
+ public void testBindingModeWithPlaceholder() throws Exception {
+ Properties prop = new Properties();
+ prop.put("myMode", "off");
+ context.getPropertiesComponent().setInitialProperties(prop);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().host("localhost");
+ rest("/b").bindingMode("{{myMode}}").get().to("mock:b");
+ }
+ });
+ context.start();
+
+
assertThat(context.getRestDefinitions().get(0).getBindingMode()).isEqualTo("{{myMode}}");
+ getMockEndpoint("mock:b").expectedBodiesReceived("Hello");
+ template.sendBody("seda:get-b", "Hello");
+ assertMockEndpointsSatisfied();
+ }
+
+ @Test
+ public void testConsumesAndProducesFromBindingModeOfRestConfiguration()
throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
restConfiguration().host("localhost").bindingMode(RestBindingMode.json);
+
rest("/p").post().type(String.class).outType(String.class).to("mock:p");
+ }
+ });
+
assertThat(restRoute().getRestBindingDefinition().getConsumes()).isEqualTo("application/json");
+
assertThat(restRoute().getRestBindingDefinition().getProduces()).isEqualTo("application/json");
+ }
+
+ @Test
+ public void testInlinedRouteKeepsStreamCacheOfVerb() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().host("localhost").inlineRoutes(true);
+ rest("/s").get().to("direct:s");
+
getRestCollection().getRests().get(0).getVerbs().get(0).setStreamCache("true");
+
+ from("direct:s").to("mock:s");
+ }
+ });
+ assertThat(restRoute().getStreamCache()).isEqualTo("true");
+ }
+
+ @Test
+ public void testAcceptWithParametersAndClientRequestValidation() throws
Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
restConfiguration().host("localhost").clientRequestValidation(true);
+ rest("/q").produces("application/json").get().to("mock:q");
+ }
+ });
+ context.start();
+
+ getMockEndpoint("mock:q").expectedMessageCount(1);
+ Exchange out = template.request("seda:get-q",
+ e -> e.getMessage().setHeader("Accept",
"application/xml;q=0.9, application/json"));
+
assertThat(out.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+ assertMockEndpointsSatisfied();
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
index 250dab693745..787fe04f8a3f 100644
---
a/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/support/http/RestUtilTest.java
@@ -47,4 +47,15 @@ public class RestUtilTest {
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json,application/xml",
"application/json"));
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json,application/xml",
"application/xml"));
}
+
+ @Test
+ public void testRestUtilWithParameters() {
+ // the parameters of each media type (such as q or charset) are not
part of the match
+ Assertions.assertTrue(
+ RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml;q=0.9, application/json"));
+
Assertions.assertTrue(RestUtil.isValidOrAcceptedContentType("application/json",
"application/json; charset=UTF-8"));
+
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml;q=0.9, text/plain"));
+ // an empty part does not match
+
Assertions.assertFalse(RestUtil.isValidOrAcceptedContentType("application/json",
"application/xml,"));
+ }
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/support/processor/RestBindingAdviceEmptyXmlTest.java
b/core/camel-core/src/test/java/org/apache/camel/support/processor/RestBindingAdviceEmptyXmlTest.java
new file mode 100644
index 000000000000..456cc5275866
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/support/processor/RestBindingAdviceEmptyXmlTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.support.processor;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RestBindingAdviceEmptyXmlTest {
+
+ @Test
+ public void testEmptyRootElement() {
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement("<a/>")).isTrue();
+
assertThat(RestBindingAdvice.isEmptyXmlRootElement("<a></a>")).isTrue();
+
assertThat(RestBindingAdvice.isEmptyXmlRootElement("<order></order>")).isTrue();
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement("<?xml
version=\"1.0\"?><a/>")).isTrue();
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement("<?xml
version=\"1.0\"?>\n<a></a>")).isTrue();
+ }
+
+ @Test
+ public void testNotEmptyRootElement() {
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement(null)).isFalse();
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement("")).isFalse();
+
assertThat(RestBindingAdvice.isEmptyXmlRootElement("<a>x</a>")).isFalse();
+
assertThat(RestBindingAdvice.isEmptyXmlRootElement("<a><b/></a>")).isFalse();
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement("<?xml
version=\"1.0\"?><a>x</a>")).isFalse();
+ assertThat(RestBindingAdvice.isEmptyXmlRootElement("not
xml")).isFalse();
+ }
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
b/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
index efc00beaf0ae..6ae69a5fb452 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/http/RestUtil.java
@@ -37,20 +37,14 @@ public final class RestUtil {
return true;
}
- // content-type is before optional charset
- target = StringHelper.before(target, ";", target);
-
valid = valid.toLowerCase(Locale.ENGLISH);
target = target.toLowerCase(Locale.ENGLISH);
- if (valid.contains(target)) {
- return true;
- }
-
- // try each part of the target
+ // try each part of the target (such as an accept header with more
media types)
for (String part : target.split(",")) {
- part = part.trim();
- if (valid.contains(part)) {
+ // the media type is before its optional parameters (such as
charset or q)
+ part = StringHelper.before(part, ";", part).trim();
+ if (!part.isEmpty() && valid.contains(part)) {
return true;
}
}
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 204ace3990ea..c362e646c703 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
@@ -440,20 +440,10 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
setOutputDataType(exchange, new DataType("xml"));
if (enableNoContentResponse) {
- String body =
MessageHelper.extractBodyAsString(exchange.getMessage()).replace("\n", "");
- if (ObjectHelper.isNotEmpty(body)) {
- int open = 0;
- int close = body.indexOf('>');
- // xml declaration
- if (body.startsWith("<?xml")) {
- open = close;
- close = body.indexOf('>', close + 1);
- }
- // empty root element <el/> or <el></el>
- if (body.length() == close + 1 || body.length() ==
(open + 1 + 2 * (close - open) + 1)) {
-
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 204);
- exchange.getMessage().setBody("");
- }
+ String body =
MessageHelper.extractBodyAsString(exchange.getMessage());
+ if (isEmptyXmlRootElement(body)) {
+
exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 204);
+ exchange.getMessage().setBody("");
}
}
}
@@ -486,6 +476,30 @@ public class RestBindingAdvice extends ServiceSupport
implements CamelInternalPr
}
}
+ /**
+ * Whether the xml is only an empty root element, such as
<tt><el/></tt> or <tt><el></el></tt>
+ * (after an optional xml declaration).
+ */
+ static boolean isEmptyXmlRootElement(String xml) {
+ if (ObjectHelper.isEmpty(xml)) {
+ return false;
+ }
+ String body = xml.replace("\n", "");
+ int start = 0;
+ int close = body.indexOf('>');
+ // xml declaration
+ if (body.startsWith("<?xml")) {
+ start = close + 1;
+ close = body.indexOf('>', start);
+ }
+ if (close < 0) {
+ return false;
+ }
+ // <el/> or <el></el> where the end tag is one char longer than the
start tag
+ int len = close - start + 1;
+ return body.length() == close + 1 || body.length() == start + 2 * len
+ 1;
+ }
+
private void setOutputDataType(Exchange exchange, DataType type) {
Message target = exchange.getMessage();
if (target instanceof DataTypeAware dataTypeAware) {
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 a5f89a84886d..ef160f30c70a 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
@@ -219,6 +219,14 @@ packages not inferred from the configured protocol.
Do not use `*`, as it disables Avro's class-loading protection.
+=== Rest DSL - consumes and produces from the binding mode of the rest
configuration
+
+When a verb has `type` or `outType` and the binding mode is `json`, `xml` or
`json_xml`, the verb's `consumes` and
+`produces` are set to the matching media types if they are not already set.
This now also happens when the binding
+mode is only set with `restConfiguration()`, as it already did when it is set
on the `rest` or the verb.
+With `clientRequestValidation` enabled, such a verb now rejects a request with
another `Content-Type` (415) or
+`Accept` (406) header.
+
=== CamelEvent JSON serialization
`CamelEvent` now provides `asJSon()` and `toJSon(int indent)` with default
implementations that