This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch jq in repository https://gitbox.apache.org/repos/asf/camel.git
commit abff636874b3b89f7bc14d494daa8233632b23c9 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Nov 24 09:18:37 2023 +0100 CAMEL-18768: Add jq function to simple language to make it easy to grab some data from JSon message body and use simple for basic JSon transformation --- components/camel-jq/src/main/docs/jq-language.adoc | 43 +++++++++++++++- .../org/apache/camel/language/jq/JqExpression.java | 3 ++ .../org/apache/camel/language/jq/JqFunctions.java | 27 ++++++++++ .../camel/language/jq/JqSimpleTransformTest.java | 57 ++++++++++++++++++++++ .../modules/languages/pages/simple-language.adoc | 3 ++ .../simple/ast/SimpleFunctionExpression.java | 20 ++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) diff --git a/components/camel-jq/src/main/docs/jq-language.adoc b/components/camel-jq/src/main/docs/jq-language.adoc index ea0f33a0633..5f876cfaad8 100644 --- a/components/camel-jq/src/main/docs/jq-language.adoc +++ b/components/camel-jq/src/main/docs/jq-language.adoc @@ -11,7 +11,7 @@ *Since Camel {since}* -Camel supports https://stedolan.github.io/jq[JQ] to allow using xref:manual::expression.adoc[Expression] or xref:manual::predicate.adoc[Predicate] on JSON messages. +Camel supports https://jqlang.github.io/jq/[JQ] to allow using xref:manual::expression.adoc[Expression] or xref:manual::predicate.adoc[Predicate] on JSON messages. == JQ Options @@ -57,10 +57,13 @@ from("direct:start") == Camel supplied JQ Functions +NOTE: JQ comes with about a hundred built-in functions, and you can see many examples from https://jqlang.github.io/jq/[JQ] documentation. + The camel-jq adds the following functions: * `header` - Allow to access the Message header in a JQ expression. * `property` - Allow to access the Exchange property in a JQ expression. +* `constant` - Allow to use a constant value as-is in a JQ expression. For example, to set the property foo with the value from the Message header `MyHeader': @@ -82,6 +85,44 @@ from("direct:start") .to("mock:result"); ---- +And using a constant value + +[source, java] +---- +from("direct:start") + .transform() + .jq(".foo = constant(\"Hello World\")") + .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 +Camel simple and JQ language as: + +[source] +---- +{ + "company": "${jq(.customer.name)}", + "location": "${jq(.customer.address.country)}", + "gold": ${jq(.customer.orders[] | length > 5)} +} +---- + +Here we use the simple language to define the structure and use JQ as inlined functions via the `${jq(exp)}` syntax. + +This makes it possible to use simple as a template language to define a basic structure and then JQ to grab the data +from an incoming JSon message. The output of the transformation is also JSon, but with simple you could +also make it XML or plain text based: + +[source,xml] +---- +<customer gold="${jq(.customer.orders[] | length > 5)}"> + <company>${jq(.customer.name)}</company> + <location>${jq(.customer.address.country)}</location> +</customer> +---- + == Dependencies If you use Maven you could just add the following to your `pom.xml`, substituting the version number for the latest and greatest release (see the download page for the latest versions). diff --git a/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqExpression.java b/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqExpression.java index b11611330c6..06689ca9bfd 100644 --- a/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqExpression.java +++ b/components/camel-jq/src/main/java/org/apache/camel/language/jq/JqExpression.java @@ -36,6 +36,7 @@ import org.apache.camel.TypeConverter; import org.apache.camel.spi.ExpressionResultTypeAware; import org.apache.camel.support.CamelContextHelper; import org.apache.camel.support.ExpressionAdapter; +import org.apache.camel.support.MessageHelper; public class JqExpression extends ExpressionAdapter implements ExpressionResultTypeAware { @@ -217,6 +218,8 @@ public class JqExpression extends ExpressionAdapter implements ExpressionResultT if (payload == null) { throw new InvalidPayloadException(exchange, JsonNode.class); } + // if body is stream cached then reset, so we can re-read it again + MessageHelper.resetStreamCache(exchange.getMessage()); } else { if (headerName != null) { payload = exchange.getMessage().getHeader(headerName, JsonNode.class); 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 114d0312a84..82d5ce182fa 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 @@ -30,6 +30,7 @@ import net.thisptr.jackson.jq.Scope; import net.thisptr.jackson.jq.Version; import net.thisptr.jackson.jq.Versions; import net.thisptr.jackson.jq.exception.JsonQueryException; +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; @@ -84,6 +85,8 @@ public final class JqFunctions { scope.addFunction(Header.NAME, 2, new Header()); scope.addFunction(Property.NAME, 1, new Property()); scope.addFunction(Property.NAME, 2, new Property()); + scope.addFunction(Constant.NAME, 1, new Constant()); + scope.addFunction(Constant.NAME, 2, new Constant()); } public abstract static class ExchangeAwareFunction implements Function { @@ -227,4 +230,28 @@ public final class JqFunctions { } } } + + /** + * A function that returns a constant value as part of JQ expression evaluation. + * + * As example, the following JQ expression sets the {@code .name} property to the constant value Donald. + * + * <pre> + * {@code + * .name = constant(\"Donald\")" + * } + * </pre> + * + */ + public static class Constant implements Function { + public static final String NAME = "constant"; + + @Override + public void apply(Scope scope, List<Expression> args, JsonNode in, Path path, PathOutput output, Version version) + throws JsonQueryException { + FunctionCall fc = (FunctionCall) args.get(0); + String t = fc.toString(); + output.emit(new TextNode(t), null); + } + } } diff --git a/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqSimpleTransformTest.java b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqSimpleTransformTest.java new file mode 100644 index 00000000000..46c94c7c7d2 --- /dev/null +++ b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqSimpleTransformTest.java @@ -0,0 +1,57 @@ +/* + * 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 JqSimpleTransformTest extends JqTestSupport { + + private static String EXPECTED = """ + { + "roll": 123, + "country": "sweden", + "fullname": "scott" + }"""; + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .transform().simple(""" + { + "roll": ${jq(.id)}, + "country": "${jq(.country // constant('sweden'))}", + "fullname": "${jq(.name)}" + }""") + .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 b4825370a24..4325100dd0f 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 @@ -252,6 +252,9 @@ If no type is given the default is used. It is also possible to use a custom `Uu and bind the bean to the xref:manual::registry.adoc[Registry] with an id. For example `${uuid(myGenerator}` where the ID is _myGenerator_. +|jq(exp) | Object | When working with JSon data, then this allows to use the xref:languages::jq-language.adoc[JQ] language +for example to extract data from the message body (in JSon format). This requires having camel-jq JAR on the classpath. + |======================================================================= == 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 f6b659fb725..edf10b84a0c 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 @@ -88,6 +88,11 @@ public class SimpleFunctionExpression extends LiteralExpression { if (answer != null) { return answer; } + // custom languages + answer = createSimpleCustomLanguage(function, strict); + if (answer != null) { + return answer; + } // camelContext OGNL String remainder = ifStartsWithReturnRemainder("camelContext", function); @@ -446,6 +451,21 @@ public class SimpleFunctionExpression extends LiteralExpression { return null; } + private Expression createSimpleCustomLanguage(String function, boolean strict) { + // jq + String remainder = ifStartsWithReturnRemainder("jq(", function); + if (remainder != null) { + String exp = StringHelper.beforeLast(remainder, ")"); + if (exp == null) { + throw new SimpleParserException("Valid syntax: ${jq(exp)} was: " + function, token.getIndex()); + } + exp = StringHelper.removeQuotes(exp); + return ExpressionBuilder.languageExpression("jq", exp); + } + + return null; + } + private Expression createSimpleExpressionDirectly(CamelContext camelContext, String expression) { if (ObjectHelper.isEqualToAny(expression, "body", "in.body")) { return ExpressionBuilder.bodyExpression();
