This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch ga in repository https://gitbox.apache.org/repos/asf/camel.git
commit 475b930085ce404011eb81b8e855107b668ad4c0 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Mar 11 17:01:31 2026 +0100 camel-groovy - Using groovyJson can work with simple jsonpath for basic jsonpath expressions. Add support for path from json array object as root. --- .../groovy/json/GroovyJsonBulkConverterLoader.java | 126 +++++++++++++++++++++ .../services/org/apache/camel/TypeConverterLoader | 2 + .../camel/groovy/json/GroovyJsonConverter.java | 68 +++++++++++ .../groovy/json/GroovySimpleJSonArrayOnlyTest.java | 61 ++++++++++ .../groovy/json/GroovySimpleJSonArrayTest.java | 62 ++++++++++ .../camel/groovy/json/GroovySimpleJSonTest.java | 77 +++++++++++++ .../language/simple/SimpleExpressionBuilder.java | 11 +- .../org/apache/camel/util/json/JsonObject.java | 40 +++++-- .../apache/camel/util/json/JsonObjectPathTest.java | 29 +++++ 9 files changed, 465 insertions(+), 11 deletions(-) diff --git a/components/camel-groovy/src/generated/java/org/apache/camel/groovy/json/GroovyJsonBulkConverterLoader.java b/components/camel-groovy/src/generated/java/org/apache/camel/groovy/json/GroovyJsonBulkConverterLoader.java new file mode 100644 index 000000000000..12575f79009c --- /dev/null +++ b/components/camel-groovy/src/generated/java/org/apache/camel/groovy/json/GroovyJsonBulkConverterLoader.java @@ -0,0 +1,126 @@ +/* Generated by camel build tools - do NOT edit this file! */ +package org.apache.camel.groovy.json; + +import javax.annotation.processing.Generated; + +import org.apache.camel.CamelContext; +import org.apache.camel.CamelContextAware; +import org.apache.camel.DeferredContextBinding; +import org.apache.camel.Exchange; +import org.apache.camel.Ordered; +import org.apache.camel.TypeConversionException; +import org.apache.camel.TypeConverterLoaderException; +import org.apache.camel.TypeConverter; +import org.apache.camel.spi.TypeConvertible; +import org.apache.camel.spi.BulkTypeConverters; +import org.apache.camel.spi.TypeConverterLoader; +import org.apache.camel.spi.TypeConverterRegistry; + +/** + * Generated by camel build tools - do NOT edit this file! + */ +@Generated("org.apache.camel.maven.packaging.TypeConverterLoaderGeneratorMojo") +@SuppressWarnings("unchecked") +@DeferredContextBinding +public final class GroovyJsonBulkConverterLoader implements TypeConverterLoader, BulkTypeConverters, CamelContextAware { + + private CamelContext camelContext; + + public GroovyJsonBulkConverterLoader() { + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + public CamelContext getCamelContext() { + return camelContext; + } + + @Override + public int size() { + return 6; + } + + @Override + public void load(TypeConverterRegistry registry) throws TypeConverterLoaderException { + registry.addBulkTypeConverters(this); + doRegistration(registry); + } + + @Override + public <T> T convertTo(Class<?> from, Class<T> to, Exchange exchange, Object value) throws TypeConversionException { + try { + Object obj = doConvertTo(from, to, exchange, value); + return (T) obj; + } catch (TypeConversionException e) { + throw e; + } catch (Exception e) { + throw new TypeConversionException(value, to, e); + } + } + + private Object doConvertTo(Class<?> from, Class<?> to, Exchange exchange, Object value) throws Exception { + if (to == org.apache.camel.util.json.JsonArray.class) { + if (value instanceof java.util.List) { + return org.apache.camel.groovy.json.GroovyJsonConverter.convertToJsonArray((java.util.List) value, exchange); + } + } else if (to == org.apache.camel.util.json.JsonObject.class) { + if (value instanceof groovy.util.Node) { + return org.apache.camel.groovy.json.GroovyJsonConverter.convertToJsonObject((groovy.util.Node) value, exchange); + } + if (value instanceof org.apache.groovy.json.internal.LazyMap) { + return org.apache.camel.groovy.json.GroovyJsonConverter.convertToJsonObject((org.apache.groovy.json.internal.LazyMap) value, exchange); + } + } else if (to == org.apache.camel.util.json.Jsonable.class) { + if (value instanceof groovy.util.Node) { + return org.apache.camel.groovy.json.GroovyJsonConverter.convertToJson((groovy.util.Node) value, exchange); + } + if (value instanceof org.apache.groovy.json.internal.LazyMap) { + return org.apache.camel.groovy.json.GroovyJsonConverter.convertToJson((org.apache.groovy.json.internal.LazyMap) value, exchange); + } + if (value instanceof java.util.List) { + return org.apache.camel.groovy.json.GroovyJsonConverter.convertToJson((java.util.List) value, exchange); + } + } + return null; + } + + private void doRegistration(TypeConverterRegistry registry) { + registry.addConverter(new TypeConvertible<>(java.util.List.class, org.apache.camel.util.json.JsonArray.class), this); + registry.addConverter(new TypeConvertible<>(groovy.util.Node.class, org.apache.camel.util.json.JsonObject.class), this); + registry.addConverter(new TypeConvertible<>(org.apache.groovy.json.internal.LazyMap.class, org.apache.camel.util.json.JsonObject.class), this); + registry.addConverter(new TypeConvertible<>(groovy.util.Node.class, org.apache.camel.util.json.Jsonable.class), this); + registry.addConverter(new TypeConvertible<>(org.apache.groovy.json.internal.LazyMap.class, org.apache.camel.util.json.Jsonable.class), this); + registry.addConverter(new TypeConvertible<>(java.util.List.class, org.apache.camel.util.json.Jsonable.class), this); + } + + public TypeConverter lookup(Class<?> to, Class<?> from) { + if (to == org.apache.camel.util.json.JsonArray.class) { + if (from == java.util.List.class) { + return this; + } + } else if (to == org.apache.camel.util.json.JsonObject.class) { + if (from == groovy.util.Node.class) { + return this; + } + if (from == org.apache.groovy.json.internal.LazyMap.class) { + return this; + } + } else if (to == org.apache.camel.util.json.Jsonable.class) { + if (from == groovy.util.Node.class) { + return this; + } + if (from == org.apache.groovy.json.internal.LazyMap.class) { + return this; + } + if (from == java.util.List.class) { + return this; + } + } + return null; + } + +} diff --git a/components/camel-groovy/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader b/components/camel-groovy/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader new file mode 100644 index 000000000000..3b5b6a8c4acd --- /dev/null +++ b/components/camel-groovy/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +org.apache.camel.groovy.json.GroovyJsonBulkConverterLoader diff --git a/components/camel-groovy/src/main/java/org/apache/camel/groovy/json/GroovyJsonConverter.java b/components/camel-groovy/src/main/java/org/apache/camel/groovy/json/GroovyJsonConverter.java new file mode 100644 index 000000000000..795d6aece289 --- /dev/null +++ b/components/camel-groovy/src/main/java/org/apache/camel/groovy/json/GroovyJsonConverter.java @@ -0,0 +1,68 @@ +/* + * 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.groovy.json; + +import java.util.List; + +import groovy.util.Node; +import org.apache.camel.Converter; +import org.apache.camel.Exchange; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsonable; +import org.apache.groovy.json.internal.LazyMap; + +@Converter(generateBulkLoader = true) +public class GroovyJsonConverter { + + /** + * Utility classes should not have a public constructor. + */ + private GroovyJsonConverter() { + } + + @Converter(order = 1) + public static JsonObject convertToJsonObject(Node node, Exchange exchange) throws Exception { + return NodeToJsonHelper.nodeToJson(node); + } + + @Converter(order = 2) + public static Jsonable convertToJson(Node node, Exchange exchange) throws Exception { + return NodeToJsonHelper.nodeToJson(node); + } + + @Converter(order = 3) + public static JsonObject convertToJsonObject(LazyMap node, Exchange exchange) throws Exception { + return new JsonObject(node); + } + + @Converter(order = 4) + public static JsonArray convertToJsonArray(List<?> node, Exchange exchange) throws Exception { + return new JsonArray(node); + } + + @Converter(order = 5) + public static Jsonable convertToJson(LazyMap node, Exchange exchange) throws Exception { + return new JsonObject(node); + } + + @Converter(order = 6) + public static Jsonable convertToJson(List<?> node, Exchange exchange) throws Exception { + return new JsonArray(node); + } + +} diff --git a/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonArrayOnlyTest.java b/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonArrayOnlyTest.java new file mode 100644 index 000000000000..bd3dfbeb3770 --- /dev/null +++ b/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonArrayOnlyTest.java @@ -0,0 +1,61 @@ +/* + * 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.groovy.json; + +import java.util.List; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +public class GroovySimpleJSonArrayOnlyTest extends CamelTestSupport { + + private static final String ARRAY_ONLY + = """ + [ "Red", "Green", "Blue" ] + """; + + @Test + public void testGroovySimpleJsonPath() throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(1); + getMockEndpoint("mock:result").message(0).body().isInstanceOf(List.class); // groovy json array + getMockEndpoint("mock:result").expectedHeaderReceived("c1", "Red"); + getMockEndpoint("mock:result").expectedHeaderReceived("c2", "Green"); + getMockEndpoint("mock:result").expectedHeaderReceived("c3", "Blue"); + + template.sendBody("direct:start", ARRAY_ONLY); + + MockEndpoint.assertIsSatisfied(context); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:start") + .unmarshal().groovyJson() + .setHeader("c1", simple("${simpleJsonpath([0])}")) + .setHeader("c2", simple("${simpleJsonpath([1])}")) + .setHeader("c3", simple("${simpleJsonpath([2])}")) + .to("mock:result"); + } + }; + } +} diff --git a/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonArrayTest.java b/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonArrayTest.java new file mode 100644 index 000000000000..34daf8492989 --- /dev/null +++ b/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonArrayTest.java @@ -0,0 +1,62 @@ +/* + * 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.groovy.json; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.groovy.json.internal.LazyMap; +import org.junit.jupiter.api.Test; + +public class GroovySimpleJSonArrayTest extends CamelTestSupport { + + private static final String COUNTRIES + = """ + { + "countries": [ "Denmark", "Sweden", "Norway" ] + } + """; + + @Test + public void testGroovySimpleJsonPath() throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(1); + getMockEndpoint("mock:result").message(0).body().isInstanceOf(LazyMap.class); // groovy json object + getMockEndpoint("mock:result").expectedHeaderReceived("c1", "Denmark"); + getMockEndpoint("mock:result").expectedHeaderReceived("c2", "Sweden"); + getMockEndpoint("mock:result").expectedHeaderReceived("c3", "Norway"); + + template.sendBody("direct:start", COUNTRIES); + + MockEndpoint.assertIsSatisfied(context); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:start") + .unmarshal().groovyJson() + .setHeader("c1", simple("${simpleJsonpath(countries[0])}")) + .setHeader("c2", simple("${simpleJsonpath(countries[1])}")) + .setHeader("c3", simple("${simpleJsonpath(countries[2])}")) + .to("mock:result"); + } + }; + } +} diff --git a/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonTest.java b/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonTest.java new file mode 100644 index 000000000000..48f35358ad8f --- /dev/null +++ b/components/camel-groovy/src/test/java/org/apache/camel/groovy/json/GroovySimpleJSonTest.java @@ -0,0 +1,77 @@ +/* + * 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.groovy.json; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.apache.groovy.json.internal.LazyMap; +import org.junit.jupiter.api.Test; + +public class GroovySimpleJSonTest extends CamelTestSupport { + + private static final String BOOKS + = """ + { + "library": { + "book": [ + { + "title": "No Title", + "author": "F. Scott Fitzgerald", + "year": "1925", + "genre": "Classic", + "id": "bk101" + }, + { + "title": "1984", + "author": "George Orwell", + "year": "1949", + "genre": "Dystopian", + "id": "bk102" + } + ] + } + } + """; + + @Test + public void testGroovySimpleJsonPath() throws Exception { + getMockEndpoint("mock:result").expectedMessageCount(1); + getMockEndpoint("mock:result").message(0).body().isInstanceOf(LazyMap.class); // groovy json object + getMockEndpoint("mock:result").expectedHeaderReceived("title1", "No Title"); + getMockEndpoint("mock:result").expectedHeaderReceived("title2", "1984"); + + template.sendBody("direct:start", BOOKS); + + MockEndpoint.assertIsSatisfied(context); + } + + @Override + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + from("direct:start") + .unmarshal().groovyJson() + .setHeader("title1", simple("${simpleJsonpath(library.book[0].title)}")) + .setHeader("title2", simple("${simpleJsonpath(library.book[1].title)}")) + .to("mock:result"); + } + }; + } +} diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java index 38a2d575a9d6..81ff6c0225a7 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionBuilder.java @@ -72,7 +72,9 @@ import org.apache.camel.util.OgnlHelper; import org.apache.camel.util.SkipIterator; import org.apache.camel.util.StringHelper; import org.apache.camel.util.StringQuoteHelper; +import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsonable; /** * Expression builder used by the simple language. @@ -3408,9 +3410,14 @@ public final class SimpleExpressionBuilder { @Override public Object evaluate(Exchange exchange) { - JsonObject jo = input.evaluate(exchange, JsonObject.class); - if (jo != null) { + Jsonable j = input.evaluate(exchange, Jsonable.class); + if (j instanceof JsonObject jo) { return jo.path(path); + } else if (j instanceof JsonArray ja) { + // wrap array in pseudo root to leverage json-path here + JsonObject jo = new JsonObject(); + jo.put("_root_", ja); + return jo.path("_root_." + path); } return null; } diff --git a/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java b/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java index 9cfce48e9924..d361e0b05179 100644 --- a/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java +++ b/tooling/camel-util-json/src/main/java/org/apache/camel/util/json/JsonObject.java @@ -23,6 +23,7 @@ import java.math.BigDecimal; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -146,7 +147,7 @@ public class JsonObject extends LinkedHashMap<String, Object> implements Jsonabl Object answer = null; boolean optional = path.startsWith("?"); - JsonObject jo = this; + Map jo = this; String sub = path; if (optional && !path.contains(".") && !path.contains("[")) { sub = sub.substring(1); @@ -161,8 +162,8 @@ public class JsonObject extends LinkedHashMap<String, Object> implements Jsonabl java.util.Optional<Object> o = doPath(path.substring(0, pos)); if (o.isPresent()) { answer = o.get(); - if (answer instanceof JsonObject) { - jo = (JsonObject) answer; + if (answer instanceof Map) { + jo = (Map) answer; } } else { optional = true; @@ -173,13 +174,34 @@ public class JsonObject extends LinkedHashMap<String, Object> implements Jsonabl java.util.Optional<Object> o = doPath(path); if (o.isPresent()) { answer = o.get(); - if (answer instanceof JsonObject) { - jo = (JsonObject) answer; + if (answer instanceof Map) { + jo = (Map) answer; } } else { optional = true; } } + + // last part can be an index + if (sub.startsWith("[")) { + int pos = -1; + String num = sub.substring(1, sub.length() - 1); + if ("last".equals(num)) { + pos = Integer.MAX_VALUE; + } else { + pos = Integer.parseInt(num); + } + if (pos != -1 && answer instanceof List<?> arr) { + jo = null; + if (pos == Integer.MAX_VALUE) { + answer = arr.getLast(); + } else if (pos < arr.size()) { + answer = arr.get(pos); + } else { + answer = null; + } + } + } if (jo != null) { answer = jo.get(sub); } @@ -233,12 +255,12 @@ public class JsonObject extends LinkedHashMap<String, Object> implements Jsonabl } part = part.substring(0, part.lastIndexOf('[')); } - if (answer instanceof JsonObject jo) { - answer = pos == -1 ? jo.getMap(part) : jo.getJsonArray(part); + if (answer instanceof Map jo) { + answer = jo.get(part); } else { - answer = pos == -1 ? getMap(part) : getJsonArray(part); + answer = get(part); } - if (pos != -1 && answer instanceof JsonArray arr) { + if (pos != -1 && answer instanceof List<?> arr) { if (pos == Integer.MAX_VALUE) { answer = arr.getLast(); } else if (pos < arr.size()) { diff --git a/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java b/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java index cea128dbb011..7d3498797fb9 100644 --- a/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java +++ b/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java @@ -56,6 +56,11 @@ public class JsonObjectPathTest { } """; + private static final String ARRAY_ONLY + = """ + [ "Red", "Green", "Blue" ] + """; + @Test public void testPath() throws Exception { JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); @@ -172,4 +177,28 @@ public class JsonObjectPathTest { } } + @Test + public void testPathArrayOnly() throws Exception { + JsonArray jo = (JsonArray) Jsoner.deserialize(ARRAY_ONLY); + Assertions.assertNotNull(jo); + Assertions.assertEquals(3, jo.size()); + + // wrap in root + JsonObject wrap = new JsonObject(); + wrap.put("_root_", jo); + + Assertions.assertEquals("Red", wrap.path("_root_.[0]")); + Assertions.assertEquals("Green", wrap.path("_root_.[1]")); + Assertions.assertEquals("Blue", wrap.path("_root_.[2]")); + Assertions.assertEquals("Blue", wrap.path("_root_.[last]")); + Assertions.assertNull(wrap.path("_root_?.[3]")); + try { + wrap.path("_root_.[3]"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + } + }
