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