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>&lt;el/&gt;</tt> or <tt>&lt;el&gt;&lt;/el&gt;</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

Reply via email to