From bff3c8238b941541ca65cf76ad5da1f22e197e7d Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 5 Mar 2024 11:55:54 +0100 Subject: [PATCH] CAMEL-20508: camel-jsonpath - Align how it converts to resultType like camel-jq --- components/camel-jsonpath/pom.xml | 5 + .../camel/jsonpath/JsonPathExpression.java | 55 +++++---- .../jsonpath/JsonPathExpressionPojoTest.java | 105 ++++++++++++++++++ .../camel/jsonpath/JsonPathLanguageTest.java | 6 +- .../pages/camel-4x-upgrade-guide-4_5.adoc | 17 +++ 5 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathExpressionPojoTest.java diff --git a/components/camel-jsonpath/pom.xml b/components/camel-jsonpath/pom.xml index fc7feb78e5983..d080b26bc1484 100644 --- a/components/camel-jsonpath/pom.xml +++ b/components/camel-jsonpath/pom.xml @@ -72,6 +72,11 @@ camel-test-spring-junit5 test + + org.apache.camel + camel-jackson + test + org.apache.camel camel-platform-http-vertx diff --git a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java index cbd451d7c2c1f..0a94ccacf0a74 100644 --- a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java +++ b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java @@ -19,6 +19,8 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import com.jayway.jsonpath.Option; import org.apache.camel.CamelContext; @@ -153,31 +155,40 @@ public void setOptions(Option[] options) { public Object evaluate(Exchange exchange) { try { Object result = evaluateJsonPath(exchange, engine); - if (resultType != null) { - if (unpackArray) { - // in some cases we get a single element that is wrapped in a List, so unwrap that - // if we for example want to grab the single entity and convert that to a int/boolean/String etc - boolean resultTypeIsCollection = Collection.class.isAssignableFrom(resultType); - boolean singleElement = result instanceof List && ((List) result).size() == 1; - if (singleElement && !resultTypeIsCollection) { - result = ((List) result).get(0); - LOG.trace("Unwrapping result: {} from single element List before converting to: {}", result, - resultType); - } - } else { - // special for List - boolean resultTypeIsCollection = Collection.class.isAssignableFrom(resultType); - boolean resultIsCollection = result instanceof List; - if (resultTypeIsCollection && !resultIsCollection) { - var list = new LinkedList<>(); - list.add(result); - result = list; - } + boolean resultTypeIsCollection = resultType != null && Collection.class.isAssignableFrom(resultType); + if (unpackArray) { + // in some cases we get a single element that is wrapped in a List, so unwrap that + // if we for example want to grab the single entity and convert that to an int/boolean/String etc + boolean singleElement = result instanceof List && ((List) result).size() == 1; + if (singleElement && !resultTypeIsCollection) { + result = ((List) result).get(0); + LOG.trace("Unwrapping result: {} from single element List before converting to: {}", result, + resultType); } - return exchange.getContext().getTypeConverter().convertTo(resultType, exchange, result); - } else { + } + if (resultType == null) { return result; } + if (resultTypeIsCollection) { + // we want a list as output + boolean resultIsCollection = result instanceof List; + if (!resultIsCollection) { + var list = new LinkedList<>(); + list.add(result); + result = list; + } + return exchange.getContext().getTypeConverter().convertTo(resultType, exchange, result); + } else if (result instanceof Collection col) { + // convert each element in the list + result = col.stream() + .filter(Objects::nonNull) // skip null + .map(item -> exchange.getContext().getTypeConverter().convertTo(resultType, exchange, item)) + .collect(Collectors.toList()); + } + if (result instanceof Collection col && col.size() == 1) { + result = col.stream().findFirst().get(); + } + return exchange.getContext().getTypeConverter().convertTo(resultType, exchange, result); } catch (Exception e) { throw new ExpressionEvaluationException(this, exchange, e); } diff --git a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathExpressionPojoTest.java b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathExpressionPojoTest.java new file mode 100644 index 0000000000000..8848b65d6a634 --- /dev/null +++ b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathExpressionPojoTest.java @@ -0,0 +1,105 @@ +/* + * 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.jsonpath; + +import java.util.Objects; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.jackson.JacksonConstants; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.Test; + +public class JsonPathExpressionPojoTest extends CamelTestSupport { + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext answer = super.createCamelContext(); + answer.getGlobalOptions().put(JacksonConstants.ENABLE_TYPE_CONVERTER, "true"); + answer.getGlobalOptions().put(JacksonConstants.TYPE_CONVERTER_TO_POJO, "true"); + return answer; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .transform().jsonpath(".book", Book.class) + .to("mock:result"); + } + }; + } + + @Test + public void testExpression() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived(new Book("foo", "bar")); + + template.sendBody("direct:start", "{\"book\":{\"author\":\"foo\",\"title\":\"bar\"}}"); + + MockEndpoint.assertIsSatisfied(context); + } + + public static class Book { + String author; + String title; + + public Book() { + } + + public Book(String author, String title) { + this.author = author; + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Book)) { + return false; + } + Book book = (Book) o; + return Objects.equals(getAuthor(), book.getAuthor()) && Objects.equals(getTitle(), book.getTitle()); + } + + @Override + public int hashCode() { + return Objects.hash(getAuthor(), getTitle()); + } + } + +} diff --git a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathLanguageTest.java b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathLanguageTest.java index badc320399c2b..e0c582c2435b3 100644 --- a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathLanguageTest.java +++ b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathLanguageTest.java @@ -145,7 +145,7 @@ public void testUnpackJsonArray() { JsonPathLanguage language = (JsonPathLanguage) context.resolveLanguage("jsonpath"); Expression expression = language.createExpression("$.store.book", - new Object[] { String.class, null, null, null, null, null, true }); + new Object[] { null, null, null, null, null, null, true, true }); String json = expression.evaluate(exchange, String.class); // check that a single json object is returned, not an array @@ -155,12 +155,12 @@ public void testUnpackJsonArray() { @Test public void testDontUnpackJsonArray() { Exchange exchange = new DefaultExchange(context); - exchange.getIn().setBody(new File("src/test/resources/expensive.json")); + exchange.getIn().setBody(new File("src/test/resources/books.json")); JsonPathLanguage language = (JsonPathLanguage) context.resolveLanguage("jsonpath"); Expression expression = language.createExpression("$.store.book", - new Object[] { String.class, null, null, null, false }); + new Object[] { null, null, null, null, false, true }); String json = expression.evaluate(exchange, String.class); // check that an array is returned, not a single object diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc index bcd6ff122b87c..9229bcda79721 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_5.adoc @@ -117,6 +117,23 @@ moved into a new group `camel.debug` with more options to configure the backlog To enable backlog tracing you should now set `camel.trace.enabled=true` instead of `camel.main.backlogTracing=true`. +=== camel-jsonpath + +The `camel-jsonpath` will now work more similar as `camel-jq` when you specify a `resultType` and have a list of entities. +Before `camel-jsonapath` would attempt to convert the `List` to the given `restultType` which often is not useable. What +users want is to be able to convert each entry in the list to a given type such as a POJO. + +For example the snippet below select all books from a JSon document, which will be in a `List` object where each +book is an entry as a `Map`. Before Camel would attempt to convert `List` to `Book` which would not be possible. +From this release onwards, Camel will convert each entry to a `Book` so the result is `List`. + +This is also how `camel-jq` works. + +[source,java] +---- +.transform().jsonpath(".book", Book.class) +---- + === camel-kamelet Routes created by Kamelets are no longer registered as JMX MBeans to avoid cluttering up with unwanted MBeans, as a Kamelet