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.
*