This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new f15e56b2cf3b [backport camel-4.22.x] CAMEL-25979: camel-language - a 
script from the CamelLanguageScript header is used once, never cached as the 
endpoint's script, and removed after use (#29873)
f15e56b2cf3b is described below

commit f15e56b2cf3b2d96074fbfab95740302cd637eb6
Author: Guillaume Nodet <[email protected]>
AuthorDate: Sat Nov 14 20:37:19 2026 +0100

    [backport camel-4.22.x] CAMEL-25979: camel-language - a script from the 
CamelLanguageScript header is used once, never cached as the endpoint's script, 
and removed after use (#29873)
    
    …eader is used once, never cached as the endpoint's script, and removed 
after use (#29751)
    
    With allowTemplateFromHeader=true, the language producer takes the script 
from the CamelLanguageScript header. A String script was compiled twice; with 
cacheScript=true the second compile was stored as the endpoint's cached 
expression, so with a resource: script later exchanges without the header ran 
the header's script; and the header stayed on the message, so the next language 
endpoint with allowTemplateFromHeader ran the same script again.
    
    LanguageProducer now handles a header script on its own path: it removes 
the header, compiles the script once, evaluates it for this exchange only and 
stops it after use. The endpoint's script, its cache and the 
contentResolvedFromResource flag are no longer touched by a header script. The 
velocity, freemarker and xslt components handle their template headers the same 
way. The 4.24 upgrade guide notes that a route that relied on the header 
staying on the message must set it again.
    
    Claude Code on behalf of davsclaus.
    
    (cherry picked from commit 7bf9e7fe70b059a1b0aa2882f1b997d0d124918f)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/components/language.json  |   2 +-
 .../camel/catalog/docs/language-component.adoc     |   3 +
 .../apache/camel/component/language/language.json  |   2 +-
 .../src/main/docs/language-component.adoc          |   3 +
 .../component/language/LanguageConstants.java      |   4 +-
 .../camel/component/language/LanguageProducer.java |  80 +++++++----
 .../language/LanguageScriptInHeaderCacheTest.java  | 149 +++++++++++++++++++++
 .../dsl/LanguageEndpointBuilderFactory.java        |   4 +-
 8 files changed, 217 insertions(+), 30 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/language.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/language.json
index 5ae03dadfa08..718f7a9ae423 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/language.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/language.json
@@ -30,7 +30,7 @@
     "autowiredEnabled": { "index": 3, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "headers": {
-    "CamelLanguageScript": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String or 
Expression", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The script to execute provided in the header. 
Takes precedence over script configured on the endpoint.", "constantName": 
"org.apache.camel.component.language.LanguageConstants#LANGUAGE_SCRIPT" }
+    "CamelLanguageScript": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String or 
Expression", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The script to execute provided in the header. 
Takes precedence over script configured on the endpoint. The script is used for 
this message only, it is never cached as the script of the endpoint (also with 
cacheScript=true), and  [...]
   },
   "properties": {
     "languageName": { "index": 0, "kind": "path", "displayName": "Language 
Name", "group": "producer", "label": "", "required": true, "type": "enum", 
"javaType": "java.lang.String", "enum": [ "bean", "constant", "csimple", 
"datasonnet", "exchangeProperty", "file", "groovy", "header", "hl7terser", 
"java", "joor", "jq", "js", "jsonpath", "mvel", "ognl", "python", "ref", 
"simple", "spel", "tokenize", "variable", "wasm", "xpath", "xquery", 
"xtokenize" ], "deprecated": false, "deprecationNote [...]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/language-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/language-component.adoc
index 435519cea521..47792957d7be 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/language-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/language-component.adoc
@@ -168,6 +168,9 @@ Object out = 
producer.requestBodyAndHeader("language:xpath", "<foo>Hello World</
 assertEquals("Hello World", out);
 ----
 
+A script from the header is used for that message only: it is never cached as 
the script of the endpoint, also with
+`cacheScript=true`, and the header is removed after use, so a later language 
endpoint runs its own script.
+
 == Loading scripts from resources
 
 You can specify a resource uri for a script to load in either the
diff --git 
a/components/camel-language/src/generated/resources/META-INF/org/apache/camel/component/language/language.json
 
b/components/camel-language/src/generated/resources/META-INF/org/apache/camel/component/language/language.json
index 5ae03dadfa08..718f7a9ae423 100644
--- 
a/components/camel-language/src/generated/resources/META-INF/org/apache/camel/component/language/language.json
+++ 
b/components/camel-language/src/generated/resources/META-INF/org/apache/camel/component/language/language.json
@@ -30,7 +30,7 @@
     "autowiredEnabled": { "index": 3, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "headers": {
-    "CamelLanguageScript": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String or 
Expression", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The script to execute provided in the header. 
Takes precedence over script configured on the endpoint.", "constantName": 
"org.apache.camel.component.language.LanguageConstants#LANGUAGE_SCRIPT" }
+    "CamelLanguageScript": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "String or 
Expression", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The script to execute provided in the header. 
Takes precedence over script configured on the endpoint. The script is used for 
this message only, it is never cached as the script of the endpoint (also with 
cacheScript=true), and  [...]
   },
   "properties": {
     "languageName": { "index": 0, "kind": "path", "displayName": "Language 
Name", "group": "producer", "label": "", "required": true, "type": "enum", 
"javaType": "java.lang.String", "enum": [ "bean", "constant", "csimple", 
"datasonnet", "exchangeProperty", "file", "groovy", "header", "hl7terser", 
"java", "joor", "jq", "js", "jsonpath", "mvel", "ognl", "python", "ref", 
"simple", "spel", "tokenize", "variable", "wasm", "xpath", "xquery", 
"xtokenize" ], "deprecated": false, "deprecationNote [...]
diff --git a/components/camel-language/src/main/docs/language-component.adoc 
b/components/camel-language/src/main/docs/language-component.adoc
index 435519cea521..47792957d7be 100644
--- a/components/camel-language/src/main/docs/language-component.adoc
+++ b/components/camel-language/src/main/docs/language-component.adoc
@@ -168,6 +168,9 @@ Object out = 
producer.requestBodyAndHeader("language:xpath", "<foo>Hello World</
 assertEquals("Hello World", out);
 ----
 
+A script from the header is used for that message only: it is never cached as 
the script of the endpoint, also with
+`cacheScript=true`, and the header is removed after use, so a later language 
endpoint runs its own script.
+
 == Loading scripts from resources
 
 You can specify a resource uri for a script to load in either the
diff --git 
a/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageConstants.java
 
b/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageConstants.java
index dc5ca43e5ce4..03c0eec90ec0 100644
--- 
a/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageConstants.java
+++ 
b/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageConstants.java
@@ -22,7 +22,9 @@ import org.apache.camel.spi.Metadata;
 public final class LanguageConstants {
 
     @Metadata(description = "The script to execute provided in the header. 
Takes precedence over\n" +
-                            "script configured on the endpoint.",
+                            "script configured on the endpoint. The script is 
used for this message only, it is never\n" +
+                            "cached as the script of the endpoint (also with 
cacheScript=true), and the header is removed\n" +
+                            "after use.",
               javaType = "String or Expression")
     public static final String LANGUAGE_SCRIPT = Exchange.LANGUAGE_SCRIPT;
 
diff --git 
a/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageProducer.java
 
b/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageProducer.java
index fb478bbec88a..b2de01d37893 100644
--- 
a/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageProducer.java
+++ 
b/components/camel-language/src/main/java/org/apache/camel/component/language/LanguageProducer.java
@@ -44,24 +44,15 @@ public class LanguageProducer extends DefaultProducer {
 
     @Override
     public void process(Exchange exchange) throws Exception {
-        String script = null;
+        if (getEndpoint().isAllowTemplateFromHeader() && 
processHeaderScript(exchange)) {
+            return;
+        }
 
-        // is there a custom expression in the header?
+        String script = null;
         Expression exp = null;
-        if (getEndpoint().isAllowTemplateFromHeader()) {
-            exp = 
exchange.getIn().getHeader(LanguageConstants.LANGUAGE_SCRIPT, Expression.class);
-            if (exp == null) {
-                script = 
exchange.getIn().getHeader(LanguageConstants.LANGUAGE_SCRIPT, String.class);
-                if (script != null) {
-                    // the script may be a file: so resolve it before using
-                    script = getEndpoint().resolveScript(script);
-                    exp = getEndpoint().getLanguage().createExpression(script);
-                    exp.init(getEndpoint().getCamelContext());
-                }
-            }
-        }
-        // if not fallback to use expression from endpoint
-        if (exp == null && getEndpoint().isCacheScript()) {
+
+        // use the cached expression from endpoint
+        if (getEndpoint().isCacheScript()) {
             exp = getEndpoint().getExpression();
         }
 
@@ -114,16 +105,7 @@ public class LanguageProducer extends DefaultProducer {
         // the result is either the result of the expression or the input 
stream as-is because its binary content
         Object result;
         if (exp != null) {
-            try {
-                result = exp.evaluate(exchange, resultType);
-                LOG.debug("Evaluated expression as: {} with: {}", result, 
exchange);
-            } finally {
-                if (!getEndpoint().isCacheScript()) {
-                    // some languages add themselves as a service which we 
then need to remove if we are not cached
-                    ServiceHelper.stopService(exp);
-                    getEndpoint().getCamelContext().removeService(exp);
-                }
-            }
+            result = evaluate(exchange, exp, !getEndpoint().isCacheScript());
         } else {
             // use the result as-is
             result = is;
@@ -135,6 +117,52 @@ public class LanguageProducer extends DefaultProducer {
         }
     }
 
+    /**
+     * Evaluates the script of the {@link LanguageConstants#LANGUAGE_SCRIPT} 
header, if there is one. The script is for
+     * this exchange only: it is compiled once, never cached as the script of 
the endpoint, and the header is removed so
+     * a later language endpoint does not run it again.
+     *
+     * @return true if the header had a script, false to use the script of the 
endpoint
+     */
+    private boolean processHeaderScript(Exchange exchange) throws Exception {
+        Expression exp = 
exchange.getIn().getHeader(LanguageConstants.LANGUAGE_SCRIPT, Expression.class);
+        boolean compiled = false;
+        if (exp == null) {
+            String script = 
exchange.getIn().getHeader(LanguageConstants.LANGUAGE_SCRIPT, String.class);
+            if (script == null) {
+                return false;
+            }
+            // the script may be a file: so resolve it before using
+            script = getEndpoint().resolveScript(script);
+            Class<?> type = resultType != Object.class ? resultType : null;
+            exp = getEndpoint().getLanguage().createExpression(script, new 
Object[] { type });
+            exp.init(getEndpoint().getCamelContext());
+            compiled = true;
+        }
+        exchange.getIn().removeHeader(LanguageConstants.LANGUAGE_SCRIPT);
+
+        // the expression compiled from the script is used once, so it is 
always stopped after use
+        Object result = evaluate(exchange, exp, compiled || 
!getEndpoint().isCacheScript());
+        if (getEndpoint().isTransform()) {
+            exchange.getMessage().setBody(result);
+        }
+        return true;
+    }
+
+    private Object evaluate(Exchange exchange, Expression exp, boolean 
stopAfterUse) throws Exception {
+        try {
+            Object result = exp.evaluate(exchange, resultType);
+            LOG.debug("Evaluated expression as: {} with: {}", result, 
exchange);
+            return result;
+        } finally {
+            if (stopAfterUse) {
+                // some languages add themselves as a service which we then 
need to remove if we are not cached
+                ServiceHelper.stopService(exp);
+                getEndpoint().getCamelContext().removeService(exp);
+            }
+        }
+    }
+
     @Override
     public LanguageEndpoint getEndpoint() {
         return (LanguageEndpoint) super.getEndpoint();
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/language/LanguageScriptInHeaderCacheTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/language/LanguageScriptInHeaderCacheTest.java
new file mode 100644
index 000000000000..c5efc9c72293
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/language/LanguageScriptInHeaderCacheTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.language;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.LanguageSupport;
+import org.apache.camel.support.builder.ExpressionBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * A script from the {@link Exchange#LANGUAGE_SCRIPT} header is for that 
exchange only: it is compiled once, it never
+ * replaces the script of the endpoint (also with cacheScript=true), and the 
header is removed after use.
+ */
+public class LanguageScriptInHeaderCacheTest extends ContextTestSupport {
+
+    private final CountingLanguage counting = new CountingLanguage();
+
+    @Override
+    protected Registry createCamelRegistry() throws Exception {
+        Registry registry = super.createCamelRegistry();
+        registry.bind("counting", counting);
+        return registry;
+    }
+
+    @Test
+    public void testResourceScriptCachedNotReplacedByHeader() throws Exception 
{
+        MockEndpoint mock = getMockEndpoint("mock:resource");
+        mock.expectedBodiesReceived("Hello A", "Bye B", "Hello C", "Hello D");
+
+        template.sendBody("direct:resource", "A");
+        template.sendBodyAndHeader("direct:resource", "B", 
Exchange.LANGUAGE_SCRIPT, "Bye ${body}");
+        template.sendBody("direct:resource", "C");
+        template.sendBody("direct:resource", "D");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testInlineScriptExpressionNotReplacedByHeader() throws 
Exception {
+        LanguageEndpoint endpoint = context.getEndpoint(inlineUri(), 
LanguageEndpoint.class);
+        Expression before = endpoint.getExpression();
+
+        MockEndpoint mock = getMockEndpoint("mock:inline");
+        mock.expectedBodiesReceived("Hello A", "Bye B", "Hello C");
+
+        template.sendBody("direct:inline", "A");
+        template.sendBodyAndHeader("direct:inline", "B", 
Exchange.LANGUAGE_SCRIPT, "Bye ${body}");
+        template.sendBody("direct:inline", "C");
+
+        assertMockEndpointsSatisfied();
+        assertSame(before, endpoint.getExpression());
+    }
+
+    @Test
+    public void testHeaderRemovedAfterUse() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:chain");
+        mock.expectedBodiesReceived("Second Bye B");
+
+        template.sendBodyAndHeader("direct:chain", "B", 
Exchange.LANGUAGE_SCRIPT, "Bye ${body}");
+
+        assertMockEndpointsSatisfied();
+        
assertNull(mock.getReceivedExchanges().get(0).getMessage().getHeader(Exchange.LANGUAGE_SCRIPT));
+    }
+
+    @Test
+    public void testHeaderScriptCompiledOnce() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:counting");
+        mock.expectedBodiesReceived("from header");
+
+        int before = counting.created.get();
+        template.sendBodyAndHeader("direct:counting", "A", 
Exchange.LANGUAGE_SCRIPT, "from header");
+
+        assertMockEndpointsSatisfied();
+        assertEquals(1, counting.created.get() - before);
+    }
+
+    private static String inlineUri() {
+        return "language:simple:" + URLEncoder.encode("Hello ${body}", 
StandardCharsets.UTF_8)
+               + "?allowTemplateFromHeader=true&cacheScript=true";
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:resource")
+                        
.to("language:simple:resource:classpath:org/apache/camel/component/language/mysimplescript.txt"
+                            + "?allowTemplateFromHeader=true&cacheScript=true")
+                        .to("mock:resource");
+
+                from("direct:inline").to(inlineUri()).to("mock:inline");
+
+                from("direct:chain")
+                        .to("language:simple:" + URLEncoder.encode("First 
${body}", StandardCharsets.UTF_8)
+                            + "?allowTemplateFromHeader=true")
+                        .to("language:simple:" + URLEncoder.encode("Second 
${body}", StandardCharsets.UTF_8)
+                            + "?allowTemplateFromHeader=true")
+                        .to("mock:chain");
+
+                
from("direct:counting").to("language:counting:endpoint?allowTemplateFromHeader=true").to("mock:counting");
+            }
+        };
+    }
+
+    private static final class CountingLanguage extends LanguageSupport {
+
+        private final AtomicInteger created = new AtomicInteger();
+
+        @Override
+        public Predicate createPredicate(String expression) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Expression createExpression(String expression) {
+            created.incrementAndGet();
+            return ExpressionBuilder.constantExpression(expression);
+        }
+    }
+}
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LanguageEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LanguageEndpointBuilderFactory.java
index 778618d7ef7a..ae2ad29ac2aa 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LanguageEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/LanguageEndpointBuilderFactory.java
@@ -424,7 +424,9 @@ public interface LanguageEndpointBuilderFactory {
 
         /**
          * The script to execute provided in the header. Takes precedence over
-         * script configured on the endpoint.
+         * script configured on the endpoint. The script is used for this
+         * message only, it is never cached as the script of the endpoint (also
+         * with cacheScript=true), and the header is removed after use.
          * 
          * The option is a: {@code String or Expression} type.
          * 

Reply via email to