This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch var-headers in repository https://gitbox.apache.org/repos/asf/camel.git
commit a6ea966df852c01f1b1808f1cc95af1ac1d741df Author: Claus Ibsen <[email protected]> AuthorDate: Tue Jan 30 14:46:11 2024 +0100 CAMEL-19749: variables - Should also copy message headers into variable when using EIP variables --- .../org/apache/camel/language/jq/JqFunctions.java | 2 +- .../language/jq/JqSimpleTransformVariableTest.java | 54 ++++++++++++++ .../modules/languages/pages/simple-language.adoc | 12 ++++ .../simple/ast/SimpleFunctionExpression.java | 18 +++++ .../org/apache/camel/language/VariableTest.java | 16 +++-- .../processor/PollEnrichVariableHeadersTest.java | 2 +- .../org/apache/camel/support/AbstractExchange.java | 3 +- .../org/apache/camel/support/ExchangeHelper.java | 14 +++- .../camel/support/ExchangeVariableRepository.java | 82 +++++++++++++++++----- ...pository.java => HeaderVariableRepository.java} | 12 ++-- .../camel/support/builder/ExpressionBuilder.java | 65 +++++++++++++++++ 11 files changed, 245 insertions(+), 35 deletions(-) diff --git a/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqFunctions.java b/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqFunctions.java index 82d5ce182fa..8589ed7eeeb 100644 --- a/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqFunctions.java +++ b/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqFunctions.java @@ -181,7 +181,7 @@ public final class JqFunctions { * * <pre> * {@code - * .name = proeprty(\"CommitterName\")" + * .name = property(\"CommitterName\")" * } * </pre> * diff --git a/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqSimpleTransformVariableTest.java b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqSimpleTransformVariableTest.java new file mode 100644 index 00000000000..5e62dbad94f --- /dev/null +++ b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqSimpleTransformVariableTest.java @@ -0,0 +1,54 @@ +/* + * 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.language.jq; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.junit.jupiter.api.Test; + +public class JqSimpleTransformVariableTest extends JqTestSupport { + + private static String EXPECTED = """ + { + "country": "se", + }"""; + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .setVariable("place", constant("{ \"name\": \"sweden\", \"iso\": \"se\" }")) + .transform().simple(""" + { + "country": "${jq(variable:place,.iso)}", + }""") + .to("mock:result"); + } + }; + } + + @Test + public void testTransform() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived(EXPECTED); + + template.sendBody("direct:start", "{\"id\": 123, \"age\": 42, \"name\": \"scott\"}"); + + MockEndpoint.assertIsSatisfied(context); + } +} diff --git a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc index 9a663faf367..2036d642303 100644 --- a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc +++ b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-language.adoc @@ -274,12 +274,24 @@ The algorithm can be SHA-256 (default) or SHA3-256. |jsonpath(exp) | Object | When working with JSon data, then this allows to use the JsonPath language for example to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. +|jsonpath(input,exp) | Object | When working with JSon data, then this allows to use the JsonPath language +for example to extract data from the message body (in JSon format). This requires having camel-jsonpath JAR on the classpath. +For _input_ you can choose `header:key`, `exchangeProperty:key` or `variable:key` to use as input for the JSon payload instead of the message body. + |jq(exp) | Object | When working with JSon data, then this allows to use the JQ language for example to extract data from the message body (in JSon format). This requires having camel-jq JAR on the classpath. +|jq(input,exp) | Object | When working with JSon data, then this allows to use the JQ language +for example to extract data from the message body (in JSon format). This requires having camel-jq JAR on the classpath. +For _input_ you can choose `header:key`, `exchangeProperty:key` or `variable:key` to use as input for the JSon payload instead of the message body. + |xpath(exp) | Object | When working with XML data, then this allows to use the XPath language for example to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. +|xpath(input,exp) | Object | When working with XML data, then this allows to use the XPath language +for example to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. +For _input_ you can choose `header:key`, `exchangeProperty:key` or `variable:key` to use as input for the JSon payload instead of the message body. + |======================================================================= == OGNL expression support diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java index 20b07a3d12b..5572e108b0a 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java @@ -522,6 +522,12 @@ public class SimpleFunctionExpression extends LiteralExpression { throw new SimpleParserException("Valid syntax: ${jq(exp)} was: " + function, token.getIndex()); } exp = StringHelper.removeQuotes(exp); + if (exp.startsWith("header:") || exp.startsWith("property:") || exp.startsWith("exchangeProperty:") + || exp.startsWith("variable:")) { + String input = StringHelper.before(exp, ","); + exp = StringHelper.after(exp, ","); + return ExpressionBuilder.singleInputLanguageExpression("jq", exp, input); + } return ExpressionBuilder.languageExpression("jq", exp); } // jsonpath @@ -532,6 +538,12 @@ public class SimpleFunctionExpression extends LiteralExpression { throw new SimpleParserException("Valid syntax: ${jsonpath(exp)} was: " + function, token.getIndex()); } exp = StringHelper.removeQuotes(exp); + if (exp.startsWith("header:") || exp.startsWith("property:") || exp.startsWith("exchangeProperty:") + || exp.startsWith("variable:")) { + String input = StringHelper.before(exp, ","); + exp = StringHelper.after(exp, ","); + return ExpressionBuilder.singleInputLanguageExpression("jq", exp, input); + } return ExpressionBuilder.languageExpression("jsonpath", exp); } remainder = ifStartsWithReturnRemainder("xpath(", function); @@ -541,6 +553,12 @@ public class SimpleFunctionExpression extends LiteralExpression { throw new SimpleParserException("Valid syntax: ${xpath(exp)} was: " + function, token.getIndex()); } exp = StringHelper.removeQuotes(exp); + if (exp.startsWith("header:") || exp.startsWith("property:") || exp.startsWith("exchangeProperty:") + || exp.startsWith("variable:")) { + String input = StringHelper.before(exp, ","); + exp = StringHelper.after(exp, ","); + return ExpressionBuilder.singleInputLanguageExpression("jq", exp, input); + } return ExpressionBuilder.languageExpression("xpath", exp); } diff --git a/core/camel-core/src/test/java/org/apache/camel/language/VariableTest.java b/core/camel-core/src/test/java/org/apache/camel/language/VariableTest.java index 6e28f0116f1..8a0de284b32 100644 --- a/core/camel-core/src/test/java/org/apache/camel/language/VariableTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/language/VariableTest.java @@ -20,6 +20,7 @@ import java.util.Map; import org.apache.camel.LanguageTestSupport; import org.apache.camel.language.variable.VariableLanguage; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -45,20 +46,25 @@ public class VariableTest extends LanguageTestSupport { @Test public void testVariableHeaders() throws Exception { - exchange.setVariable("myKey.header.foo", "abc"); - exchange.setVariable("myKey.header.bar", 123); + exchange.setVariable("header:myKey.foo", "abc"); + exchange.setVariable("header:myKey.bar", 123); exchange.setVariable("myOtherKey", "Hello Again"); - assertExpression("myKey.header.foo", "abc"); - assertExpression("myKey.header.bar", 123); + assertEquals("abc", exchange.getVariable("header:myKey.foo")); + assertEquals(123, exchange.getVariable("header:myKey.bar")); - Map map = exchange.getVariable("myKey.headers", Map.class); + Map map = exchange.getVariable("header:myKey", Map.class); assertNotNull(map); assertEquals(2, map.size()); assertEquals("abc", map.get("foo")); assertEquals(123, map.get("bar")); } + @Test + public void testInvalidHeaderKey() { + Assertions.assertThrows(IllegalArgumentException.class, () -> exchange.getVariable("header:")); + } + @Test public void testSingleton() { VariableLanguage prop = new VariableLanguage(); diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/PollEnrichVariableHeadersTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/PollEnrichVariableHeadersTest.java index e754ec464ac..cadd647e286 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/PollEnrichVariableHeadersTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/PollEnrichVariableHeadersTest.java @@ -30,7 +30,7 @@ public class PollEnrichVariableHeadersTest extends ContextTestSupport { getMockEndpoint("mock:after").expectedVariableReceived("bye", "Bye World"); getMockEndpoint("mock:result").expectedBodiesReceived("Bye World"); getMockEndpoint("mock:result").expectedVariableReceived("bye", "Bye World"); - getMockEndpoint("mock:result").expectedVariableReceived("bye.header.echo", "CamelCamel"); + getMockEndpoint("mock:result").expectedVariableReceived("header:bye.echo", "CamelCamel"); getMockEndpoint("mock:result").message(0).header("echo").isNull(); template.sendBody("direct:receive", "World"); diff --git a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java index 4a7f67fb68f..a0367cd5400 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java @@ -128,8 +128,7 @@ abstract class AbstractExchange implements Exchange { if (this.variableRepository == null) { this.variableRepository = new ExchangeVariableRepository(getContext()); } - this.variableRepository.setVariables(parent.getVariables()); - + this.variableRepository.copyFrom(parent.variableRepository); } if (parent.hasProperties()) { this.properties = safeCopyProperties(parent.properties); diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java index 6ba9fc6bb02..7dd9f30b93e 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java @@ -1088,6 +1088,10 @@ public final class ExchangeHelper { public static void setVariable(Exchange exchange, String name, Object value) { VariableRepository repo = null; String id = StringHelper.before(name, ":"); + // header and exchange is reserved + if ("header".equals(id) || "exchange".equals(id)) { + id = null; + } if (id != null) { VariableRepositoryFactory factory = exchange.getContext().getCamelContextExtension().getContextPlugin(VariableRepositoryFactory.class); @@ -1113,6 +1117,10 @@ public final class ExchangeHelper { public static void setVariableFromMessageBodyAndHeaders(Exchange exchange, String name, Message message) { VariableRepository repo = null; String id = StringHelper.before(name, ":"); + // header and exchange is reserved + if ("header".equals(id) || "exchange".equals(id)) { + id = null; + } if (id != null) { VariableRepositoryFactory factory = exchange.getContext().getCamelContextExtension().getContextPlugin(VariableRepositoryFactory.class); @@ -1128,7 +1136,7 @@ public final class ExchangeHelper { Object body = message.getBody(); va.setVariable(name, body); for (Map.Entry<String, Object> header : message.getHeaders().entrySet()) { - String key = name + ".header." + header.getKey(); + String key = "header:" + name + "." + header.getKey(); Object value = header.getValue(); va.setVariable(key, value); } @@ -1145,6 +1153,10 @@ public final class ExchangeHelper { public static Object getVariable(Exchange exchange, String name) { VariableRepository repo = null; String id = StringHelper.before(name, ":"); + // header and exchange is reserved + if ("header".equals(id) || "exchange".equals(id)) { + id = null; + } if (id != null) { VariableRepositoryFactory factory = exchange.getContext().getCamelContextExtension().getContextPlugin(VariableRepositoryFactory.class); diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java index 1d75855542f..0e144ce3627 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java @@ -17,10 +17,10 @@ package org.apache.camel.support; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; -import org.apache.camel.StreamCache; import org.apache.camel.spi.VariableRepository; import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.util.CaseInsensitiveMap; @@ -31,38 +31,82 @@ import org.apache.camel.util.StringHelper; */ final class ExchangeVariableRepository extends AbstractVariableRepository { + private final Map<String, Object> headers = new ConcurrentHashMap<>(8); + public ExchangeVariableRepository(CamelContext camelContext) { setCamelContext(camelContext); // ensure its started ServiceHelper.startService(this); } - @Override - public String getId() { - return "exchange"; + void copyFrom(ExchangeVariableRepository source) { + setVariables(source.getVariables()); + this.headers.putAll(source.headers); } @Override public Object getVariable(String name) { - Object answer = super.getVariable(name); - if (answer == null && name.endsWith(".headers")) { - String prefix = name.substring(0, name.length() - 1) + "."; // xxx.headers -> xxx.header. - // we want all headers for a given variable - Map<String, Object> map = new CaseInsensitiveMap(); - for (Map.Entry<String, Object> entry : getVariables().entrySet()) { - String key = entry.getKey(); - if (key.startsWith(prefix)) { - key = StringHelper.after(key, prefix); - map.put(key, entry.getValue()); + String id = StringHelper.before(name, ":"); + if ("header".equals(id)) { + String prefix = StringHelper.after(name, ":"); + if (prefix == null || prefix.isBlank()) { + throw new IllegalArgumentException("Variable " + name + " must have header key"); + } + if (!prefix.contains(".")) { + prefix = prefix + "."; + // we want all headers for a given variable + Map<String, Object> map = new CaseInsensitiveMap(); + for (Map.Entry<String, Object> entry : headers.entrySet()) { + String key = entry.getKey(); + if (key.startsWith(prefix)) { + key = StringHelper.after(key, prefix); + map.put(key, entry.getValue()); + } } + return map; + } else { + return headers.get(prefix); } - return map; } - if (answer instanceof StreamCache sc) { - // reset so the cache is ready to be used as a variable - sc.reset(); + return super.getVariable(name); + } + + @Override + public void setVariable(String name, Object value) { + String id = StringHelper.before(name, ":"); + if ("header".equals(id)) { + name = StringHelper.after(name, ":"); + if (value != null) { + // avoid the NullPointException + headers.put(name, value); + } else { + // if the value is null, we just remove the key from the map + headers.remove(name); + } + } else { + super.setVariable(name, value); } - return answer; + } + + @Override + public Object removeVariable(String name) { + String id = StringHelper.before(name, ":"); + if ("header".equals(id)) { + name = StringHelper.after(name, ":"); + return headers.remove(name); + } + return super.removeVariable(name); + } + + @Override + public void clear() { + super.clear(); + headers.clear(); + } + + @Override + public String getId() { + return "exchange"; } } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java b/core/camel-support/src/main/java/org/apache/camel/support/HeaderVariableRepository.java similarity index 85% copy from core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java copy to core/camel-support/src/main/java/org/apache/camel/support/HeaderVariableRepository.java index 1d75855542f..21a691db9b7 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeVariableRepository.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/HeaderVariableRepository.java @@ -27,11 +27,11 @@ import org.apache.camel.util.CaseInsensitiveMap; import org.apache.camel.util.StringHelper; /** - * {@link VariableRepository} which is local per {@link Exchange} to hold request-scoped variables. + * {@link VariableRepository} which is local per {@link Exchange} to hold request-scoped variables for message headers. */ -final class ExchangeVariableRepository extends AbstractVariableRepository { +final class HeaderVariableRepository extends AbstractVariableRepository { - public ExchangeVariableRepository(CamelContext camelContext) { + public HeaderVariableRepository(CamelContext camelContext) { setCamelContext(camelContext); // ensure its started ServiceHelper.startService(this); @@ -39,14 +39,14 @@ final class ExchangeVariableRepository extends AbstractVariableRepository { @Override public String getId() { - return "exchange"; + return "header"; } @Override public Object getVariable(String name) { Object answer = super.getVariable(name); - if (answer == null && name.endsWith(".headers")) { - String prefix = name.substring(0, name.length() - 1) + "."; // xxx.headers -> xxx.header. + if (answer == null && !name.contains(".")) { + String prefix = name + "."; // we want all headers for a given variable Map<String, Object> map = new CaseInsensitiveMap(); for (Map.Entry<String, Object> entry : getVariables().entrySet()) { diff --git a/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java b/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java index 6ea8d094b0b..bc9775de5e5 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java @@ -53,6 +53,7 @@ import org.apache.camel.support.GroupIterator; import org.apache.camel.support.GroupTokenIterator; import org.apache.camel.support.LanguageHelper; import org.apache.camel.support.LanguageSupport; +import org.apache.camel.support.SingleInputTypedLanguageSupport; import org.apache.camel.util.InetAddressUtil; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.StringHelper; @@ -957,6 +958,70 @@ public class ExpressionBuilder { }; } + /** + * Returns an expression for evaluating the expression/predicate using the given language + * + * @param expression the expression or predicate + * @param input input to use instead of message body + * @return an expression object which will evaluate the expression/predicate using the given language + */ + public static Expression singleInputLanguageExpression(final String language, final String expression, final String input) { + return new ExpressionAdapter() { + private Expression expr; + private Predicate pred; + + @Override + public Object evaluate(Exchange exchange) { + return expr.evaluate(exchange, Object.class); + } + + @Override + public boolean matches(Exchange exchange) { + return pred.matches(exchange); + } + + @Override + public void init(CamelContext context) { + super.init(context); + Language lan = context.resolveLanguage(language); + if (lan != null) { + if (input != null && lan instanceof SingleInputTypedLanguageSupport sil) { + String prefix = StringHelper.before(input, ":"); + String source = StringHelper.after(input, ":"); + if (prefix != null) { + prefix = prefix.trim(); + } + if (source != null) { + source = source.trim(); + } + if ("header".equals(prefix)) { + sil.setHeaderName(source); + } else if ("property".equals(prefix) || "exchangeProperty".equals(prefix)) { + sil.setPropertyName(source); + } else if ("variable".equals(prefix)) { + sil.setVariableName(source); + } else { + throw new IllegalArgumentException( + "Invalid input source for language. Should either be header:key, exchangeProperty:key, or variable:key, was: " + + input); + } + } + pred = lan.createPredicate(expression); + pred.init(context); + expr = lan.createExpression(expression); + expr.init(context); + } else { + throw new NoSuchLanguageException(language); + } + } + + @Override + public String toString() { + return "language[" + language + ":" + expression + "]"; + } + }; + } + /** * Returns the expression for the exchanges inbound message body */
