This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new a3843a2ef91b [backport camel-4.18.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 (#29875)
a3843a2ef91b is described below
commit a3843a2ef91b01a6eec885dbaecd55c0c86ec050
Author: Guillaume Nodet <[email protected]>
AuthorDate: Sat Nov 14 19:15:12 2026 +0100
[backport camel-4.18.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 (#29875)
Backport of #29751
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/components/language.json | 2 +-
.../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 +-
7 files changed, 214 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 e8ab773f1476..a4c28d47bf41 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/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 e8ab773f1476..a4c28d47bf41 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 b053b0d40387..5915396883fd 100644
--- a/components/camel-language/src/main/docs/language-component.adoc
+++ b/components/camel-language/src/main/docs/language-component.adoc
@@ -88,6 +88,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 a50048ab55ec..a272a5d3a0bb 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.
*