Skip to content

Commit

Permalink
CAMEL-20508: camel-jsonpath - Align how it converts to resultType lik…
Browse files Browse the repository at this point in the history
…e camel-jq (#13384)
  • Loading branch information
davsclaus committed Mar 5, 2024
1 parent d30e9ee commit b6c1097
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 25 deletions.
5 changes: 5 additions & 0 deletions components/camel-jsonpath/pom.xml
Expand Up @@ -72,6 +72,11 @@
<artifactId>camel-test-spring-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jackson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-platform-http-vertx</artifactId>
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
@@ -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());
}
}

}
Expand Up @@ -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
Expand All @@ -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
Expand Down
Expand Up @@ -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<Map>` 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<Book>`.

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
Expand Down

0 comments on commit b6c1097

Please sign in to comment.