You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2024/01/31 09:07:28 UTC

(camel) 12/16: CAMEL-19749: variables - Should also copy message headers into variable when using EIP variables

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 <cl...@gmail.com>
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
      */