This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch jp in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9b5107f182f0111f96fb0ec5c68791701b3c37f1 Author: Claus Ibsen <[email protected]> AuthorDate: Tue Mar 10 19:39:08 2026 +0100 CAMEL-23170: camel-core - Add simple jsonpath to Camels JsonObject --- .../org/apache/camel/util/json/JsonObject.java | 176 ++++++++++++++++++++- .../apache/camel/util/json/JsonObjectPathTest.java | 136 ++++++++++++++++ 2 files changed, 311 insertions(+), 1 deletion(-) 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 e3c93873a2be..100ee9b68316 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 @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; /** * JsonObject is a common non-thread safe data format for string to data mappings. The contents of a JsonObject are only @@ -49,12 +50,185 @@ public class JsonObject extends LinkedHashMap<String, Object> implements Jsonabl * Instantiate a new JsonObject by accepting a map's entries, which could lead to de/serialization issues of the * resulting JsonObject since the entry values aren't validated as JSON values. * + * The json-path syntax also supports arrays using square brackets with the position index, such as + * "foo.bar[0].title", "foo.bar[1].title". You can use "last" as index number to get the last element of the array. + * * @param map represents the mappings to produce the JsonObject with. */ public JsonObject(final Map<String, ?> map) { super(map); } + /** + * A very basic json-path that can retrieve leaf node using a dot syntax, such as "foo.bar.title", to get the title + * attribute from the leaf JsonObject object (ie bar). + * + * The json-path syntax also supports arrays using square brackets with the position index, such as + * "foo.bar[0].title", "foo.bar[1].title". You can use "last" as index number to get the last element of the array. + * + * The returned value is expected to be a String + * + * @throws IllegalArgumentException if the path traversal does not exist + */ + public String pathString(final String path) { + boolean optional = path.startsWith("?"); + JsonObject jo = this; + String sub = path; + if (path.contains(".")) { + // grab until last dot + int pos = path.lastIndexOf("."); + optional |= '?' == path.charAt(pos - 1); + sub = path.substring(pos + 1); + if (optional) { + pos = pos - 1; + } + java.util.Optional<JsonObject> o = doPath(path.substring(0, pos)); + if (o.isPresent()) { + jo = o.get(); + } else { + optional = true; + jo = null; + } + } + String answer = jo != null ? jo.getString(sub) : null; + if (answer == null && !optional) { + throw new IllegalArgumentException("JSonObject path " + path + " is null"); + } + return answer; + } + + /** + * A very basic json-path that can retrieve leaf node using a dot syntax, such as "foo.bar.title", to get the title + * attribute from the leaf JsonObject object (ie bar). + * + * The json-path syntax also supports arrays using square brackets with the position index, such as + * "foo.bar[0].title", "foo.bar[1].title". You can use "last" as index number to get the last element of the array. + * + * You can use ?. to mark a path as optional, which returns null instead of throwing an exception if the path + * traversal does not exist. + * + * The returned value is expected to be an Integer + * + * @throws IllegalArgumentException if the path traversal does not exist + */ + public Integer pathInteger(String path) { + boolean optional = path.startsWith("?"); + JsonObject jo = this; + String sub = path; + if (path.contains(".")) { + // grab until last dot + int pos = path.lastIndexOf("."); + optional |= '?' == path.charAt(pos - 1); + sub = path.substring(pos + 1); + if (optional) { + pos = pos - 1; + } + java.util.Optional<JsonObject> o = doPath(path.substring(0, pos)); + if (o.isPresent()) { + jo = o.get(); + } else { + optional = true; + jo = null; + } + } + Integer answer = jo != null ? jo.getInteger(sub) : null; + if (answer == null && !optional) { + throw new IllegalArgumentException("JSonObject path " + path + " is null"); + } + return answer; + } + + /** + * A very basic json-path that can retrieve leaf node using a dot syntax, such as "foo.bar.title", to get the title + * attribute from the leaf JsonObject object (ie bar). + * + * The returned value is expected to be a Boolean + * + * @throws IllegalArgumentException if the path traversal does not exist + */ + public Boolean pathBoolean(String path) { + boolean optional = path.startsWith("?"); + JsonObject jo = this; + String sub = path; + if (path.contains(".")) { + // grab until last dot + int pos = path.lastIndexOf("."); + optional |= '?' == path.charAt(pos - 1); + sub = path.substring(pos + 1); + if (optional) { + pos = pos - 1; + } + java.util.Optional<JsonObject> o = doPath(path.substring(0, pos)); + if (o.isPresent()) { + jo = o.get(); + } else { + optional = true; + jo = null; + } + } + Boolean answer = jo != null ? jo.getBoolean(sub) : null; + if (answer == null && !optional) { + throw new IllegalArgumentException("JSonObject path " + path + " is null"); + } + return answer; + } + + /** + * A very basic json-path that can retrieve a JSonObject node using a dot syntax, such as "foo.bar", to get the bar + * JSonObject. + */ + public JsonObject path(final String path) { + return doPath(path).orElse(null); + } + + private Optional<JsonObject> doPath(final String path) { + Jsonable answer = null; + + String[] split = path.splitWithDelimiters("(\\?\\.|\\.)", 0); + for (int i = 0; i < split.length; i = i + 2) { + String part = split[i]; + String dot = i > 0 ? split[i - 1] : null; + int pos = -1; + boolean optional; + if (part.startsWith("?")) { + part = part.substring(1); + optional = true; + } else { + optional = dot != null && dot.equals("?."); + } + // notice multi-level index is not supported, such as[0][0] or [0][1] + if (part.endsWith("]")) { + String num = part.substring(part.lastIndexOf('[') + 1, part.length() - 1); + if ("last".equals(num)) { + pos = Integer.MAX_VALUE; + } else { + pos = Integer.parseInt(num); + } + part = part.substring(0, part.lastIndexOf('[')); + } + if (answer instanceof JsonObject jo) { + answer = pos == -1 ? jo.getMap(part) : jo.getJsonArray(part); + } else { + answer = pos == -1 ? getMap(part) : getJsonArray(part); + } + if (pos != -1 && answer instanceof JsonArray arr) { + if (pos == Integer.MAX_VALUE) { + answer = (Jsonable) arr.getLast(); + } else if (pos < arr.size()) { + answer = (Jsonable) arr.get(pos); + } else { + answer = null; + } + } + if (answer == null && !optional) { + throw new IllegalArgumentException("JSonObject path " + path + " at " + part + " does not exist"); + } else if (answer == null) { + return Optional.empty(); + } + } + return Optional.of(JsonObject.class.cast(answer)); + } + /** * A convenience method that assumes there is another JsonObject at the given key. * @@ -796,4 +970,4 @@ public class JsonObject extends LinkedHashMap<String, Object> implements Jsonabl } writable.write('}'); } -} +}; 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 new file mode 100644 index 000000000000..5f0fc66b2f51 --- /dev/null +++ b/tooling/camel-util-json/src/test/java/org/apache/camel/util/json/JsonObjectPathTest.java @@ -0,0 +1,136 @@ +/* + * 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.util.json; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +public class JsonObjectPathTest { + + private static final String BOOKS + = """ + { + "library": { + "book": [ + { + "title": "No Title", + "author": "F. Scott Fitzgerald", + "year": "1925", + "genre": "Classic", + "movie": false, + "id": "bk101" + }, + { + "title": "1984", + "author": "George Orwell", + "year": "1949", + "genre": "Dystopian", + "movie": true, + "id": "bk102" + } + ] + } + } + """; + + @Test + public void testPath() throws Exception { + JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); + + JsonObject obj = jo.path("library.book[0]"); + Assertions.assertNotNull(obj); + Assertions.assertEquals("No Title", obj.getString("title")); + + obj = jo.path("library.book[1]"); + Assertions.assertNotNull(obj); + Assertions.assertEquals("1984", obj.getString("title")); + } + + @Test + public void testPathAttribute() throws Exception { + JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); + Assertions.assertNotNull(jo); + + Assertions.assertEquals("No Title", jo.pathString("library.book[0].title")); + Assertions.assertEquals(1925, jo.pathInteger("library.book[0].year")); + Assertions.assertFalse(jo.pathBoolean("library.book[0].movie")); + Assertions.assertEquals("1984", jo.pathString("library.book[1].title")); + Assertions.assertEquals(1949, jo.pathInteger("library.book[1].year")); + Assertions.assertTrue(jo.pathBoolean("library.book[1].movie")); + + try { + Assertions.assertNull(jo.path("library.book[1].unknown")); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testPathOptional() throws Exception { + JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); + Assertions.assertNotNull(jo); + + Assertions.assertNotNull(jo.path("library?.book[0]")); + Assertions.assertNull(jo.path("library?.book[2]")); + try { + Assertions.assertNull(jo.path("library.book[2]")); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testPathAttributeOptional() throws Exception { + JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); + Assertions.assertNotNull(jo); + + Assertions.assertNull(jo.pathString("library?.book[2]?.subTitle")); + Assertions.assertEquals("No Title", jo.pathString("library.book[0].title")); + Assertions.assertNull(jo.pathString("library.book[0]?.subTitle")); + Assertions.assertNull(jo.pathString("?unknown?.book[0].title")); + + Assertions.assertNull(jo.pathString("library?.book[2].title")); + Assertions.assertNull(jo.pathInteger("library?.book[2].year")); + Assertions.assertNull(jo.pathBoolean("library?.book[2].movie")); + } + + @Test + public void testPathAttributeArrayLast() throws Exception { + JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); + Assertions.assertNotNull(jo); + + Assertions.assertEquals("No Title", jo.pathString("library.book[0].title")); + Assertions.assertEquals(1925, jo.pathInteger("library.book[0].year")); + Assertions.assertEquals("1984", jo.pathString("library.book[last].title")); + Assertions.assertEquals(1949, jo.pathInteger("library.book[last].year")); + } + + @Test + public void testPathAttributeLeafNode() throws Exception { + JsonObject jo = (JsonObject) Jsoner.deserialize(BOOKS); + Assertions.assertNotNull(jo); + + jo = (JsonObject) jo.getJsonObject("library").getJsonArray("book").get(0); + Assertions.assertEquals("No Title", jo.pathString("title")); + Assertions.assertEquals(1925, jo.pathInteger("year")); + } + +}
