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 010dcd71755 CAMEL-21928: camel-jq - include variable() and body() as new functions to access in-process data 010dcd71755 is described below commit 010dcd717550a79fb49d15dc40bb5a2d801dfad1 Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Sat Apr 5 09:53:55 2025 +0200 CAMEL-21928: camel-jq - include variable() and body() as new functions to access in-process data --- components/camel-jq/src/main/docs/jq-language.adoc | 13 +++ .../org/apache/camel/language/jq/JqFunctions.java | 95 ++++++++++++++++++++++ .../camel/language/jq/JqExpressionBodyFnTest.java | 74 +++++++++++++++++ .../language/jq/JqExpressionVariableFnTest.java | 46 +++++++++++ 4 files changed, 228 insertions(+) diff --git a/components/camel-jq/src/main/docs/jq-language.adoc b/components/camel-jq/src/main/docs/jq-language.adoc index 82ade292985..f022840bd41 100644 --- a/components/camel-jq/src/main/docs/jq-language.adoc +++ b/components/camel-jq/src/main/docs/jq-language.adoc @@ -50,6 +50,8 @@ The camel-jq adds the following functions: * `header`: allow accessing the Message header in a JQ expression. * `property`: allow accessing the Exchange property in a JQ expression. * `constant`: allow using a constant value as-is in a JQ expression. +* `variable`: allow accessing the Exchange variable in a JQ expression. +* `body`: the message body as a textual value. For example, to set the property foo with the value from the Message header `MyHeader': @@ -81,6 +83,17 @@ from("direct:start") .to("mock:result"); ---- +Or using an exchange variable: + +[source, java] +---- +from("direct:start") + .transform() + .jq(".foo = variable(\"MyVar\")") + .to("mock:result"); +---- + + === Transforming a JSon message For basic JSon transformation where you have a fixed structure, you can represent with a combination of using 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 2c28a795d38..e1b3ec86c19 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 @@ -34,6 +34,7 @@ import net.thisptr.jackson.jq.internal.tree.FunctionCall; import net.thisptr.jackson.jq.path.Path; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; +import org.apache.camel.StreamCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,6 +89,9 @@ public final class JqFunctions { scope.addFunction(Property.NAME, 2, new Property()); scope.addFunction(Constant.NAME, 1, new Constant()); scope.addFunction(Constant.NAME, 2, new Constant()); + scope.addFunction(Variable.NAME, 1, new Variable()); + scope.addFunction(Variable.NAME, 2, new Variable()); + scope.addFunction(Body.NAME, new Body()); } public abstract static class ExchangeAwareFunction implements Function { @@ -255,4 +259,95 @@ public final class JqFunctions { output.emit(new TextNode(t), null); } } + + /** + * A function that allow to retrieve an {@link org.apache.camel.Exchange} variable value as part of JQ expression + * evaluation. + * + * As example, the following JQ expression sets the {@code .name} property to the value of the variable named + * {@code CommitterName}. + * + * <pre> + * {@code + * .name = variable(\"CommitterName\")" + * } + * </pre> + * + */ + public static class Variable extends ExchangeAwareFunction { + public static final String NAME = "variable"; + + @Override + protected void doApply( + Scope scope, + List<Expression> args, + JsonNode in, + Path path, + PathOutput output, + Version version, + Exchange exchange) + throws JsonQueryException { + + args.get(0).apply(scope, in, name -> { + if (args.size() == 2) { + args.get(1).apply(scope, in, defval -> { + extract( + exchange, + name.asText(), + defval.asText(), + output); + }); + } else { + extract( + exchange, + name.asText(), + null, + output); + } + }); + } + + private void extract(Exchange exchange, String variableName, String variableValue, PathOutput output) + throws JsonQueryException { + String variable = exchange.getVariable(variableName, variableValue, String.class); + + if (variable == null) { + output.emit(NullNode.getInstance(), null); + } else { + output.emit(new TextNode(variable), null); + } + } + } + + /** + * A function that returns the message body as part of JQ expression evaluation. + * + * As example, the following JQ expression sets the {@code .name} property to the message body. + * + * <pre> + * {@code + * .name = body()" + * } + * </pre> + * + */ + public static class Body implements Function { + public static final String NAME = "body"; + + @Override + public void apply(Scope scope, List<Expression> args, JsonNode in, Path path, PathOutput output, Version version) + throws JsonQueryException { + Exchange exchange = EXCHANGE_LOCAL.get(); + if (exchange != null) { + // in case you refer to the body multiple times then we need to handle stream-caching + Object body = exchange.getMessage().getBody(); + if (body instanceof StreamCache sc) { + sc.reset(); + } + String t = exchange.getMessage().getBody(String.class); + output.emit(new TextNode(t), null); + } + } + } + } diff --git a/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqExpressionBodyFnTest.java b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqExpressionBodyFnTest.java new file mode 100644 index 00000000000..000439234ad --- /dev/null +++ b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqExpressionBodyFnTest.java @@ -0,0 +1,74 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class JqExpressionBodyFnTest extends JqTestSupport { + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + var jq = expression().jq(". + [{\"array\": body()}]").source("mydata").end(); + + from("direct:start") + .setVariable("mydata", simple("${body}")) + .setVariable("mydata", jq) + .to("mock:result"); + } + }; + } + + @Test + public void testExpression() throws Exception { + getMockEndpoint("mock:result") + .expectedMessageCount(1); + + ObjectMapper mapper = new ObjectMapper(); + ArrayNode arr = mapper.createArrayNode(); + + ObjectNode user1 = mapper.createObjectNode(); + user1.put("id", 1); + user1.put("name", "John Doe"); + arr.add(user1); + + ObjectNode user2 = mapper.createObjectNode(); + user2.put("id", 2); + user2.put("name", "Jane Jackson"); + arr.add(user2); + + template.sendBody("direct:start", arr); + + MockEndpoint.assertIsSatisfied(context); + + String res = getMockEndpoint("mock:result").getExchanges().get(0).getVariable("mydata", String.class); + // should copy the body into the array node + var n = mapper.reader().readTree(res); + var a = n.get(2); + Assertions.assertNotNull(a); + a = a.get("array"); + // the body functions return the data as text + Assertions.assertTrue(a.isTextual()); + } +} diff --git a/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqExpressionVariableFnTest.java b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqExpressionVariableFnTest.java new file mode 100644 index 00000000000..8b1003dbb4a --- /dev/null +++ b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqExpressionVariableFnTest.java @@ -0,0 +1,46 @@ +/* + * 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 JqExpressionVariableFnTest extends JqTestSupport { + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .setVariable("MyVar", constant("MyValue")) + .transform().jq(".foo = variable(\"MyVar\")") + .to("mock:result"); + } + }; + } + + @Test + public void testExpression() throws Exception { + getMockEndpoint("mock:result") + .expectedBodiesReceived(node("foo", "MyValue")); + + template.sendBody("direct:start", node("foo", "bar")); + + MockEndpoint.assertIsSatisfied(context); + } +}