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 7bf9e7fe70b0 CAMEL-25979: camel-language - a script from the 
CamelLanguageScript header is used once, never cached as the endpoint's script, 
and removed after use (#29751)
7bf9e7fe70b0 is described below

commit 7bf9e7fe70b059a1b0aa2882f1b997d0d124918f
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 21:54:33 2026 +0100

    CAMEL-25979: camel-language - a script from the CamelLanguageScript header 
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.
    
    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 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   7 +
 .../dsl/LanguageEndpointBuilderFactory.java        |   4 +-
 9 files changed, 224 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 d3e50e76cc88..ad368f524178 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, "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, "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 the header is removed a [...]
   },
   "bodyTypes": {
     "producerIn": { "carriers": [ "any" ], "description": "The body as the 
language of the script reads it. The script is in the uri, the script option or 
a resource, or in the CamelLanguageScript header with 
allowTemplateFromHeader=true, and never in the body" },
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 fba87714eb43..2ca71ce0bd47 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
@@ -169,6 +169,9 @@ Object out = 
producer.requestBodyAndHeader("language:xpath?allowTemplateFromHead
 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 d3e50e76cc88..ad368f524178 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, "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, "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 the header is removed a [...]
   },
   "bodyTypes": {
     "producerIn": { "carriers": [ "any" ], "description": "The body as the 
language of the script reads it. The script is in the uri, the script option or 
a resource, or in the CamelLanguageScript header with 
allowTemplateFromHeader=true, and never in the body" },
diff --git a/components/camel-language/src/main/docs/language-component.adoc 
b/components/camel-language/src/main/docs/language-component.adoc
index fba87714eb43..2ca71ce0bd47 100644
--- a/components/camel-language/src/main/docs/language-component.adoc
+++ b/components/camel-language/src/main/docs/language-component.adoc
@@ -169,6 +169,9 @@ Object out = 
producer.requestBodyAndHeader("language:xpath?allowTemplateFromHead
 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 cc1f910361ac..c712b286f1ec 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();
         }
 
@@ -119,16 +110,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;
@@ -143,6 +125,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/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 01eea679d7f5..2b76f4e30d41 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -8373,3 +8373,10 @@ property of the bucket -- so `InfluxDb2Producer` never 
reads the option or the h
 Both are now deprecated with a note that says so. Remove `retentionPolicy` 
from the endpoint URI and stop setting the
 header: they are silently ignored. To control how long the data is kept, set 
the retention period on the bucket the
 endpoint writes to.
+
+=== camel-language - the CamelLanguageScript header is used once and removed
+
+With `allowTemplateFromHeader=true`, a script from the `CamelLanguageScript` 
header is now used for that message only.
+Before, with `cacheScript=true` it could replace the cached script of the 
endpoint, so later messages without the header
+ran it too. The header is now removed after use, so a later language endpoint 
in the route runs its own script instead
+of the header's. Set the header again before each language endpoint that 
should use it.
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 ba0efd25a2aa..6c4666274d70 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