This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new d8e501e620 Add MarshalledNode tree façade and RFC 6901 JSON-Pointer;
remove public ObjectRest.
d8e501e620 is described below
commit d8e501e62015de9276970f11a6c19fb5ce5bec94
Author: James Bognar <[email protected]>
AuthorDate: Tue Jun 16 10:29:17 2026 -0400
Add MarshalledNode tree façade and RFC 6901 JSON-Pointer; remove public
ObjectRest.
- Add MarshalledNode typed tree façade and JsonPointer (RFC 6901)
addressing over MarshalledMap/MarshalledList.
- Relocate the ObjectRest traversal engine to internal PathTraversal;
rename ObjectRestException -> PathTraversalException.
- Remove public ObjectRest and ResponseContent.asObjectRest(); migrate
Traversable, Swagger/OpenApi findRef, and SwaggerVar/OpenApiVar to
PathTraversal.
Co-authored-by: Cursor <[email protected]>
---
.../org/apache/juneau/bean/openapi3/OpenApi.java | 2 +-
.../org/apache/juneau/bean/swagger/Swagger.java | 2 +-
.../juneau/marshall/collections/JsonPointer.java | 284 +++++++++++++++
.../marshall/collections/MarshalledList.java | 53 ++-
.../juneau/marshall/collections/MarshalledMap.java | 53 ++-
.../marshall/collections/MarshalledNode.java | 391 +++++++++++++++++++++
.../juneau/marshall/collections/package-info.java | 2 +-
.../{ObjectRest.java => PathTraversal.java} | 165 +++------
...tException.java => PathTraversalException.java} | 12 +-
.../test/java/org/apache/juneau/JsonMap_Test.java | 7 +-
.../marshall/collections/JsonPointer_Test.java | 227 ++++++++++++
.../marshall/collections/MarshalledNode_Test.java | 346 ++++++++++++++++++
...bjectRest_Test.java => PathTraversal_Test.java} | 224 +++++-------
.../apache/juneau/BasicRuntimeException_Test.java | 4 +-
.../classic/RestClient_Response_Body_Test.java | 9 +-
.../rest/client/classic/ResponseContent.java | 41 +--
.../juneau/rest/client/classic/RestClient.java | 3 -
.../juneau/rest/server/converter/Traversable.java | 7 +-
.../apache/juneau/rest/server/vars/OpenApiVar.java | 2 +-
.../apache/juneau/rest/server/vars/SwaggerVar.java | 2 +-
20 files changed, 1495 insertions(+), 341 deletions(-)
diff --git
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/OpenApi.java
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/OpenApi.java
index b5040fd62b..a1bfec4ef7 100644
---
a/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/OpenApi.java
+++
b/juneau-bean/juneau-bean-openapi-v3/src/main/java/org/apache/juneau/bean/openapi3/OpenApi.java
@@ -278,7 +278,7 @@ public class OpenApi extends OpenApiElement {
if (! ref.startsWith("#/"))
throw rex("Unsupported reference: ''{0}''", ref);
try {
- return new ObjectRest(this).get(ref.substring(1), c);
+ return new PathTraversal(this).get(ref.substring(1), c);
} catch (Exception e) {
throw bex(e, c, "Reference ''{0}'' could not be
converted to type ''{1}''.", ref, cn(c));
}
diff --git
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Swagger.java
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Swagger.java
index 1939d39dbe..55a38997fb 100644
---
a/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Swagger.java
+++
b/juneau-bean/juneau-bean-swagger-v2/src/main/java/org/apache/juneau/bean/swagger/Swagger.java
@@ -514,7 +514,7 @@ public class Swagger extends SwaggerElement {
if (! ref.startsWith("#/"))
throw rex("Unsupported reference: ''{0}''", ref);
try {
- return new ObjectRest(this).get(ref.substring(1), c);
+ return new PathTraversal(this).get(ref.substring(1), c);
} catch (Exception e) {
throw bex(e, c, "Reference ''{0}'' could not be
converted to type ''{1}''.", ref, cn(c));
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/JsonPointer.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/JsonPointer.java
new file mode 100644
index 0000000000..4d6a895b7a
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/JsonPointer.java
@@ -0,0 +1,284 @@
+/*
+ * 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.juneau.marshall.collections;
+
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
+import java.util.*;
+
+/**
+ * An <a class="doclink"
href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> JSON Pointer
that addresses a
+ * single value within a {@link Map}/{@link List} tree (such as the {@link
MarshalledMap}/{@link MarshalledList} family).
+ *
+ * <p>
+ * A JSON Pointer is a string of zero or more <c>/</c>-prefixed reference
tokens. The empty string <js>""</js> references
+ * the whole document (the root). A non-empty pointer must begin with
<c>/</c>; each token is the text between <c>/</c>
+ * separators with <c>~1</c> decoded to <c>/</c> and <c>~0</c> decoded to
<c>~</c>. For arrays a token is either a
+ * base-10 non-negative integer index or the single character <c>-</c> (which
references the nonexistent element after
+ * the last element, used to append on writes); for objects the decoded token
is the member name.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * JsonMap <jv>doc</jv> = JsonMap.<jsm>of</jsm>(<js>"foo"</js>,
JsonList.<jsm>of</jsm>(<js>"bar"</js>, <js>"baz"</js>));
+ *
+ * <jc>// Read.</jc>
+ * Object <jv>v</jv> =
JsonPointer.<jsm>of</jsm>(<js>"/foo/0"</js>).eval(<jv>doc</jv>); <jc>//
"bar"</jc>
+ *
+ * <jc>// Auto-vivifying write (creates intermediate containers).</jc>
+ * JsonPointer.<jsm>of</jsm>(<js>"/foo/-"</js>).set(<jv>doc</jv>,
<js>"qux"</js>); <jc>// appends "qux"</jc>
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe.
+ * <li>Read-miss (missing key, out-of-range index, or type mismatch)
resolves to <jk>null</jk> via {@link #eval(Object)}.
+ * </ul>
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation typed tree model layered over the
+ * {@code Marshalled*} collections and may change incompatibly in a future
release.
+ */
+public class JsonPointer {
+
+ /** Sentinel distinguishing an absent value from a present
<jk>null</jk> value during traversal. */
+ private static final Object MISSING = new Object();
+
+ /**
+ * Parses and validates the specified RFC 6901 JSON Pointer string.
+ *
+ * @param pointer The pointer string. The empty string references the
whole document; a non-empty pointer must begin with <c>/</c>.
+ * @return A new {@link JsonPointer}, never <jk>null</jk>.
+ * @throws IllegalArgumentException If the pointer is <jk>null</jk> or
malformed (non-empty and not beginning with <c>/</c>).
+ */
+ public static JsonPointer of(String pointer) {
+ return new JsonPointer(parse(pointer));
+ }
+
+ /**
+ * Encodes a raw object member name into an RFC 6901 reference token
(escapes <c>~</c> to <c>~0</c> and <c>/</c> to <c>~1</c>).
+ *
+ * @param rawMemberName The raw member name. Must not be <jk>null</jk>.
+ * @return The encoded reference token.
+ */
+ public static String encodeToken(String rawMemberName) {
+ return rawMemberName.replace("~", "~0").replace("/", "~1");
+ }
+
+ /**
+ * Decodes an RFC 6901 reference token into a raw object member name
(unescapes <c>~1</c> to <c>/</c> then <c>~0</c> to <c>~</c>).
+ *
+ * @param token The reference token. Must not be <jk>null</jk>.
+ * @return The decoded member name.
+ */
+ public static String decodeToken(String token) {
+ return token.replace("~1", "/").replace("~0", "~");
+ }
+
+ private static List<String> parse(String pointer) {
+ if (pointer == null)
+ throw illegalArg("JSON Pointer cannot be null.");
+ if (pointer.isEmpty())
+ return List.of();
+ if (pointer.charAt(0) != '/')
+ throw illegalArg("Invalid JSON Pointer ''{0}'': a
non-empty pointer must begin with ''/''.", pointer);
+ var parts = pointer.split("/", -1);
+ var tokens = new ArrayList<String>(parts.length - 1);
+ for (var i = 1; i < parts.length; i++)
+ tokens.add(decodeToken(parts[i]));
+ return tokens;
+ }
+
+ private final List<String> tokens;
+
+ private JsonPointer(List<String> tokens) {
+ this.tokens = tokens;
+ }
+
+ /**
+ * Reads the value addressed by this pointer from the specified root.
+ *
+ * <p>
+ * Walks the tokens from <c>root</c> (which may be a {@link Map}/{@link
List} such as the {@link MarshalledMap}/
+ * {@link MarshalledList} family). The empty pointer returns
<c>root</c>.
+ *
+ * @param root The root document. Can be <jk>null</jk>.
+ * @return The addressed value, or <jk>null</jk> on any missing key,
out-of-range index, or type mismatch.
+ */
+ public Object eval(Object root) {
+ var o = evalOrMissing(root);
+ return o == MISSING ? null : o;
+ }
+
+ /**
+ * Writes the specified value at the location addressed by this
pointer, auto-vivifying missing intermediate containers.
+ *
+ * <p>
+ * Intermediate containers are created based on the next token: a
numeric or <c>-</c> token implies a {@link JsonList},
+ * otherwise a {@link JsonMap}. The <c>-</c> token on a list appends.
The empty pointer cannot mutate <c>root</c> in
+ * place for the whole-document case, so it simply returns <c>value</c>
as the new root.
+ *
+ * @param root The root document. For a non-empty pointer, must be a
mutable {@link Map}/{@link List} tree.
+ * @param value The value to set.
+ * @return The (possibly newly-created) effective root.
+ */
+ public Object set(Object root, Object value) {
+ if (tokens.isEmpty())
+ return value;
+ var parent = navigateToParent(root);
+ rawSet(parent, tokens.get(tokens.size() - 1), value);
+ return root;
+ }
+
+ /**
+ * Removes the member or element addressed by this pointer.
+ *
+ * @param root The root document.
+ * @return The removed value, or <jk>null</jk> if the addressed
member/element was absent (or the pointer is the root pointer).
+ */
+ public Object remove(Object root) {
+ if (tokens.isEmpty())
+ return null;
+ var parent = root;
+ for (var i = 0; i < tokens.size() - 1; i++) {
+ parent = rawGet(parent, tokens.get(i));
+ if (parent == MISSING)
+ return null;
+ }
+ return rawRemove(parent, tokens.get(tokens.size() - 1));
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ if (tokens.isEmpty())
+ return "";
+ var sb = new StringBuilder();
+ for (var t : tokens)
+ sb.append('/').append(encodeToken(t));
+ return sb.toString();
+ }
+
+ /**
+ * Returns <jk>true</jk> if this is the root pointer (the empty pointer
addressing the whole document).
+ */
+ boolean isRoot() {
+ return tokens.isEmpty();
+ }
+
+ /**
+ * Same as {@link #eval(Object)} but returns the {@link #MISSING}
sentinel (rather than <jk>null</jk>) for an absent
+ * value, so callers can distinguish a present <jk>null</jk> value from
a read-miss.
+ */
+ Object evalOrMissing(Object root) {
+ var current = root;
+ for (var token : tokens) {
+ current = rawGet(current, token);
+ if (current == MISSING)
+ return MISSING;
+ }
+ return current;
+ }
+
+ /** Returns <jk>true</jk> if the specified value is the absent-value
sentinel returned by {@link #evalOrMissing(Object)}. */
+ static boolean isMissing(Object o) {
+ return o == MISSING;
+ }
+
+ private Object navigateToParent(Object root) {
+ var current = root;
+ for (var i = 0; i < tokens.size() - 1; i++) {
+ var token = tokens.get(i);
+ var child = rawGet(current, token);
+ if (! (child instanceof Map || child instanceof List)) {
+ child = isIndexToken(tokens.get(i + 1)) ? new
JsonList() : new JsonMap();
+ rawSet(current, token, child);
+ }
+ current = child;
+ }
+ return current;
+ }
+
+ private static Object rawGet(Object container, String token) {
+ if (container instanceof Map<?,?> container2)
+ return container2.containsKey(token) ?
container2.get(token) : MISSING;
+ if (container instanceof List<?> container2) {
+ var idx = toArrayIndex(token);
+ return (idx >= 0 && idx < container2.size()) ?
container2.get(idx) : MISSING;
+ }
+ return MISSING;
+ }
+
+ @SuppressWarnings({
+ "unchecked" // Object-node maps accept String keys / Object
values; array-node lists accept Object elements.
+ })
+ private static void rawSet(Object container, String token, Object
value) {
+ if (container instanceof Map) {
+ ((Map<String,Object>)container).put(token, value);
+ return;
+ }
+ if (container instanceof List) {
+ var list = (List<Object>)container;
+ if ("-".equals(token)) {
+ list.add(value);
+ return;
+ }
+ var idx = toArrayIndex(token);
+ if (idx < 0 || idx > list.size())
+ throw illegalArg("Invalid JSON Pointer array
index ''{0}'' for a list of size {1}.", token, list.size());
+ if (idx == list.size())
+ list.add(value);
+ else
+ list.set(idx, value);
+ return;
+ }
+ throw illegalArg("Cannot set value at token ''{0}'' on a
non-container value.", token);
+ }
+
+ private static Object rawRemove(Object container, String token) {
+ if (container instanceof Map<?,?> container2)
+ return container2.containsKey(token) ?
container2.remove(token) : null;
+ if (container instanceof List<?> container2) {
+ var idx = toArrayIndex(token);
+ return (idx >= 0 && idx < container2.size()) ?
container2.remove(idx) : null;
+ }
+ return null;
+ }
+
+ private static boolean isIndexToken(String token) {
+ return "-".equals(token) || toArrayIndex(token) >= 0;
+ }
+
+ /*
+ * Returns the non-negative array index for the specified token, or -1
if the token is not a valid RFC 6901 array
+ * index (no leading zeros except "0" itself; "-" is not a numeric
index).
+ */
+ private static int toArrayIndex(String token) {
+ var len = token.length();
+ if (len == 0)
+ return -1;
+ if (len > 1 && token.charAt(0) == '0')
+ return -1;
+ for (var i = 0; i < len; i++) {
+ var c = token.charAt(i);
+ if (c < '0' || c > '9')
+ return -1;
+ }
+ try {
+ return Integer.parseInt(token);
+ } catch (NumberFormatException e) { // HTT - guards against
overflow on very long digit strings.
+ return -1;
+ }
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledList.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledList.java
index a24ad6c53b..90c28a4c6c 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledList.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledList.java
@@ -36,7 +36,7 @@ import org.apache.juneau.marshall.serializer.*;
* <p>
* This class is the base for {@link JsonList} and the per-marshaller {@code
XList} variants. It carries
* all marshaller-neutral functionality such as typed accessors, fluent
setters, bean integration,
- * and {@link ObjectRest}-based path navigation. The default {@link
#toString()} comes from
+ * and {@link PathTraversal}-based path navigation. The default {@link
#toString()} comes from
* {@link LinkedList}; subclasses should override it to produce the
appropriate marshalled form.
*
* <p>
@@ -384,7 +384,7 @@ public class MarshalledList extends LinkedList<Object> {
transient MarshallingSession session = null;
- private transient ObjectRest objectRest;
+ private transient PathTraversal pathTraversal;
/**
* Construct an empty list.
@@ -540,6 +540,25 @@ public class MarshalledList extends LinkedList<Object> {
return this;
}
+ /**
+ * Returns the node addressed by the specified <a class="doclink"
href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>
+ * JSON Pointer relative to this list.
+ *
+ * <p>
+ * Convenience for
<c>MarshalledNode.<jsm>of</jsm>(<jk>this</jk>).at(<jv>pointer</jv>)</c>. A
read-miss returns
+ * <jk>null</jk>; an addressed JSON <jk>null</jk> value returns a null
node.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This method is part of the
next-generation typed tree model layered over the
+ * {@code Marshalled*} collections and may change incompatibly in a
future release.
+ *
+ * @param pointer The RFC 6901 JSON Pointer.
+ * @return A node wrapping the addressed value, or <jk>null</jk> if the
addressed value is absent.
+ */
+ public MarshalledNode at(String pointer) {
+ return MarshalledNode.of(this).at(pointer);
+ }
+
/**
* Serializes this list to a string using the specified serializer.
*
@@ -555,14 +574,14 @@ public class MarshalledList extends LinkedList<Object> {
* this POJO.
*
* <p>
- * This method uses the {@link ObjectRest} class to perform the lookup,
so the map can contain any of the various
- * class types that the {@link ObjectRest} class supports (e.g. beans,
collections, arrays).
+ * This method uses the {@link PathTraversal} class to perform the
lookup, so the map can contain any of the various
+ * class types that the {@link PathTraversal} class supports (e.g.
beans, collections, arrays).
*
* @param path The path to the entry.
* @return The previous value, or <jk>null</jk> if the entry doesn't
exist.
*/
public Object deleteAt(String path) {
- return getObjectRest().delete(path);
+ return getPathTraversal().delete(path);
}
/**
@@ -646,7 +665,7 @@ public class MarshalledList extends LinkedList<Object> {
* @return The value, or <jk>null</jk> if the entry doesn't exist.
*/
public <T> T getAt(String path, Class<T> type) {
- return getObjectRest().get(path, type);
+ return getPathTraversal().get(path, type);
}
/**
@@ -660,7 +679,7 @@ public class MarshalledList extends LinkedList<Object> {
* @return The value, or <jk>null</jk> if the entry doesn't exist.
*/
public <T> T getAt(String path, Type type, Type...args) {
- return getObjectRest().get(path, type, args);
+ return getPathTraversal().get(path, type, args);
}
/**
@@ -798,15 +817,15 @@ public class MarshalledList extends LinkedList<Object> {
* Similar to {@link #putAt(String,Object) putAt(String,Object)}, but
used to append to collections and arrays.
*
* <p>
- * This method uses the {@link ObjectRest} class to perform the lookup,
so the map can contain any of the various
- * class types that the {@link ObjectRest} class supports (e.g. beans,
collections, arrays).
+ * This method uses the {@link PathTraversal} class to perform the
lookup, so the map can contain any of the various
+ * class types that the {@link PathTraversal} class supports (e.g.
beans, collections, arrays).
*
* @param path The path to the entry.
* @param o The new value.
* @return The previous value, or <jk>null</jk> if the entry doesn't
exist.
*/
public Object postAt(String path, Object o) {
- return getObjectRest().post(path, o);
+ return getPathTraversal().post(path, o);
}
/**
@@ -814,15 +833,15 @@ public class MarshalledList extends LinkedList<Object> {
* in this POJO.
*
* <p>
- * This method uses the {@link ObjectRest} class to perform the lookup,
so the map can contain any of the various
- * class types that the {@link ObjectRest} class supports (e.g. beans,
collections, arrays).
+ * This method uses the {@link PathTraversal} class to perform the
lookup, so the map can contain any of the various
+ * class types that the {@link PathTraversal} class supports (e.g.
beans, collections, arrays).
*
* @param path The path to the entry.
* @param o The new value.
* @return The previous value, or <jk>null</jk> if the entry doesn't
exist.
*/
public Object putAt(String path, Object o) {
- return getObjectRest().put(path, o);
+ return getPathTraversal().put(path, o);
}
/**
@@ -864,10 +883,10 @@ public class MarshalledList extends LinkedList<Object> {
return new UnmodifiableMarshalledList(this);
}
- private ObjectRest getObjectRest() {
- if (objectRest == null)
- objectRest = new ObjectRest(this);
- return objectRest;
+ private PathTraversal getPathTraversal() {
+ if (pathTraversal == null)
+ pathTraversal = new PathTraversal(this);
+ return pathTraversal;
}
/**
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledMap.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledMap.java
index 16b775728c..0210e3e6f3 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledMap.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledMap.java
@@ -42,7 +42,7 @@ import org.apache.juneau.marshall.swap.*;
* <p>
* This class is the base for {@link JsonMap} and the per-marshaller {@code
XMap} variants. It carries
* all marshaller-neutral functionality such as typed accessors, fluent
setters, bean integration,
- * and {@link ObjectRest}-based path navigation. The default {@link
#toString()} comes from
+ * and {@link PathTraversal}-based path navigation. The default {@link
#toString()} comes from
* {@link LinkedHashMap}; subclasses should override it to produce the
appropriate marshalled form.
*
* <p>
@@ -290,7 +290,7 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
private transient MarshallingSession session;
private transient Map<String,Object> inner;
- private transient ObjectRest objectRest;
+ private transient PathTraversal pathTraversal;
private transient Predicate<Object> valueFilter = x -> true;
@@ -463,6 +463,25 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
return this;
}
+ /**
+ * Returns the node addressed by the specified <a class="doclink"
href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>
+ * JSON Pointer relative to this map.
+ *
+ * <p>
+ * Convenience for
<c>MarshalledNode.<jsm>of</jsm>(<jk>this</jk>).at(<jv>pointer</jv>)</c>. A
read-miss returns
+ * <jk>null</jk>; an addressed JSON <jk>null</jk> value returns a null
node.
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This method is part of the
next-generation typed tree model layered over the
+ * {@code Marshalled*} collections and may change incompatibly in a
future release.
+ *
+ * @param pointer The RFC 6901 JSON Pointer.
+ * @return A node wrapping the addressed value, or <jk>null</jk> if the
addressed value is absent.
+ */
+ public MarshalledNode at(String pointer) {
+ return MarshalledNode.of(this).at(pointer);
+ }
+
/**
* Serializes this map to a string using the specified serializer.
*
@@ -560,14 +579,14 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
* in this POJO.
*
* <p>
- * This method uses the {@link ObjectRest} class to perform the lookup,
so the map can contain any of the various
- * class types that the {@link ObjectRest} class supports (e.g. beans,
collections, arrays).
+ * This method uses the {@link PathTraversal} class to perform the
lookup, so the map can contain any of the various
+ * class types that the {@link PathTraversal} class supports (e.g.
beans, collections, arrays).
*
* @param path The path to the entry.
* @return The previous value, or <jk>null</jk> if the entry doesn't
exist.
*/
public Object deleteAt(String path) {
- return getObjectRest().delete(path);
+ return getPathTraversal().delete(path);
}
@Override /* Overridden from Map */
@@ -860,7 +879,7 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
* @return The value, or <jk>null</jk> if the entry doesn't exist.
*/
public <T> T getAt(String path, Class<T> type) {
- return getObjectRest().get(path, type);
+ return getPathTraversal().get(path, type);
}
/**
@@ -874,7 +893,7 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
* @return The value, or <jk>null</jk> if the entry doesn't exist.
*/
public <T> T getAt(String path, Type type, Type...args) {
- return getObjectRest().get(path, type, args);
+ return getPathTraversal().get(path, type, args);
}
/**
@@ -1327,15 +1346,15 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
* Similar to {@link #putAt(String,Object) putAt(String,Object)}, but
used to append to collections and arrays.
*
* <p>
- * This method uses the {@link ObjectRest} class to perform the lookup,
so the map can contain any of the various
- * class types that the {@link ObjectRest} class supports (e.g. beans,
collections, arrays).
+ * This method uses the {@link PathTraversal} class to perform the
lookup, so the map can contain any of the various
+ * class types that the {@link PathTraversal} class supports (e.g.
beans, collections, arrays).
*
* @param path The path to the entry.
* @param o The new value.
* @return The previous value, or <jk>null</jk> if the entry doesn't
exist.
*/
public Object postAt(String path, Object o) {
- return getObjectRest().post(path, o);
+ return getPathTraversal().post(path, o);
}
@Override
@@ -1350,15 +1369,15 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
* POJO.
*
* <p>
- * This method uses the {@link ObjectRest} class to perform the lookup,
so the map can contain any of the various
- * class types that the {@link ObjectRest} class supports (e.g. beans,
collections, arrays).
+ * This method uses the {@link PathTraversal} class to perform the
lookup, so the map can contain any of the various
+ * class types that the {@link PathTraversal} class supports (e.g.
beans, collections, arrays).
*
* @param path The path to the entry.
* @param o The new value.
* @return The previous value, or <jk>null</jk> if the entry doesn't
exist.
*/
public Object putAt(String path, Object o) {
- return getObjectRest().put(path, o);
+ return getPathTraversal().put(path, o);
}
/**
@@ -1598,10 +1617,10 @@ public class MarshalledMap extends
LinkedHashMap<String,Object> {
throw bex(cm.inner(), "Cannot convert to class type ''{0}''.
Only beans and maps can be converted using this method.", cn(cm));
}
- private ObjectRest getObjectRest() {
- if (objectRest == null)
- objectRest = new ObjectRest(this);
- return objectRest;
+ private PathTraversal getPathTraversal() {
+ if (pathTraversal == null)
+ pathTraversal = new PathTraversal(this);
+ return pathTraversal;
}
/*
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledNode.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledNode.java
new file mode 100644
index 0000000000..eb64e49cde
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/MarshalledNode.java
@@ -0,0 +1,391 @@
+/*
+ * 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.juneau.marshall.collections;
+
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.marshall.*;
+
+/**
+ * Typed tree façade over the generic-collections model ({@link MarshalledMap}
/ {@link MarshalledList} and their
+ * {@link JsonMap} / {@link JsonList} subclasses).
+ *
+ * <p>
+ * This is a <b>live, mutating view</b> (a façade — <i>not</i> a parallel node
hierarchy) wrapping a single backing
+ * value which is one of:
+ * <ul>
+ * <li>a {@link Map} (an <i>object</i> node),
+ * <li>a {@link List} (an <i>array</i> node),
+ * <li>a scalar value ({@link String}, {@link Number}, {@link Boolean},
etc.) (a <i>value</i> node), or
+ * <li><jk>null</jk> (a <i>null</i> node).
+ * </ul>
+ *
+ * <p>
+ * It gives Jackson-migrants the ergonomics they expect (node-type
introspection, typed value accessors, and fluent
+ * tree building) without replacing the underlying {@link Map}/{@link List}
representation. Because it is a live view,
+ * mutating builders such as {@link #put(String,Object)} and {@link
#add(Object)} mutate the backing container
+ * <i>in place</i> — callers holding the underlying map/list see the change.
Use {@link #copy()} for an independent
+ * deep-copy snapshot.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * <jc>// Build a tree fluently (mutates a backing JsonMap/JsonList in
place).</jc>
+ * MarshalledNode <jv>node</jv> = MarshalledNode.<jsm>objectNode</jsm>()
+ * .put(<js>"a"</js>, 1)
+ * .put(<js>"b"</js>,
MarshalledNode.<jsm>arrayNode</jsm>().add(<js>"x"</js>).add(<js>"y"</js>).value());
+ *
+ * String <jv>json</jv> = Json.<jsm>of</jsm>(<jv>node</jv>.value());
<jc>// {"a":1,"b":["x","y"]}</jc>
+ *
+ * <jc>// Navigate and read typed values.</jc>
+ * Integer <jv>a</jv> = <jv>node</jv>.get(<js>"a"</js>).asInt();
<jc>// 1</jc>
+ * String <jv>x</jv> = <jv>node</jv>.get(<js>"b"</js>).get(0).asString();
<jc>// "x"</jc>
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe.
+ * </ul>
+ *
+ * <p>
+ * <b>Beta — API subject to change:</b> This type is part of the
next-generation typed tree model layered over the
+ * {@code Marshalled*} collections and may change incompatibly in a future
release.
+ */
+public class MarshalledNode {
+
+ /**
+ * Wraps any value as a node.
+ *
+ * <p>
+ * The value is stored as-is (no copy): a {@link Map} becomes an object
node, a {@link List} becomes an array node,
+ * a non-<jk>null</jk> non-container value becomes a value node, and
<jk>null</jk> becomes a null node. For navigation
+ * and fluent building to work the value should be (or contain) the
{@link MarshalledMap}/{@link MarshalledList}
+ * family, but any {@link Map}/{@link List} is accepted.
+ *
+ * @param value The value to wrap. Can be <jk>null</jk>.
+ * @return A new node wrapping the value, never <jk>null</jk>.
+ */
+ public static MarshalledNode of(Object value) {
+ return new MarshalledNode(value);
+ }
+
+ /**
+ * Creates a new object node backed by a new empty {@link JsonMap}.
+ *
+ * @return A new object node, never <jk>null</jk>.
+ */
+ public static MarshalledNode objectNode() {
+ return new MarshalledNode(new JsonMap());
+ }
+
+ /**
+ * Creates a new array node backed by a new empty {@link JsonList}.
+ *
+ * @return A new array node, never <jk>null</jk>.
+ */
+ public static MarshalledNode arrayNode() {
+ return new MarshalledNode(new JsonList());
+ }
+
+ private final Object value;
+
+ private MarshalledNode(Object value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns <jk>true</jk> if this node wraps a {@link Map} (an object
node).
+ *
+ * @return <jk>true</jk> if this is an object node.
+ */
+ public boolean isObject() {
+ return value instanceof Map;
+ }
+
+ /**
+ * Returns <jk>true</jk> if this node wraps a {@link List} (an array
node).
+ *
+ * @return <jk>true</jk> if this is an array node.
+ */
+ public boolean isArray() {
+ return value instanceof List;
+ }
+
+ /**
+ * Returns <jk>true</jk> if this node wraps a non-<jk>null</jk> scalar
(not a {@link Map} or {@link List}).
+ *
+ * @return <jk>true</jk> if this is a value node.
+ */
+ public boolean isValue() {
+ return value != null && ! (value instanceof Map) && ! (value
instanceof List);
+ }
+
+ /**
+ * Returns <jk>true</jk> if this node wraps <jk>null</jk>.
+ *
+ * @return <jk>true</jk> if this is a null node.
+ */
+ public boolean isNull() {
+ return value == null;
+ }
+
+ /**
+ * Returns the backing value converted to a {@link String}.
+ *
+ * @return The converted value, or <jk>null</jk> if this is not a value
node or the value cannot be converted.
+ */
+ public String asString() {
+ return asScalar(String.class);
+ }
+
+ /**
+ * Returns the backing value converted to an {@link Integer}.
+ *
+ * @return The converted value, or <jk>null</jk> if this is not a value
node or the value cannot be converted.
+ */
+ public Integer asInt() {
+ return asScalar(Integer.class);
+ }
+
+ /**
+ * Returns the backing value converted to a {@link Long}.
+ *
+ * @return The converted value, or <jk>null</jk> if this is not a value
node or the value cannot be converted.
+ */
+ public Long asLong() {
+ return asScalar(Long.class);
+ }
+
+ /**
+ * Returns the backing value converted to a {@link Double}.
+ *
+ * @return The converted value, or <jk>null</jk> if this is not a value
node or the value cannot be converted.
+ */
+ public Double asDouble() {
+ return asScalar(Double.class);
+ }
+
+ /**
+ * Returns the backing value converted to a {@link Boolean}.
+ *
+ * @return The converted value, or <jk>null</jk> if this is not a value
node or the value cannot be converted.
+ */
+ public Boolean asBoolean() {
+ return asScalar(Boolean.class);
+ }
+
+ /**
+ * Converts the raw backing value to the specified type.
+ *
+ * <p>
+ * Unlike the scalar accessors, this works on object nodes too (e.g.
converting a {@link Map} object node to a bean).
+ * Routes through the same conversion machinery used by {@link
MarshalledMap#get(String,Class)}.
+ *
+ * @param <T> The type to convert to.
+ * @param type The type to convert to.
+ * @return The converted value, or <jk>null</jk> if the backing value
is <jk>null</jk> or cannot be converted.
+ */
+ public <T> T as(Class<T> type) {
+ if (value == null)
+ return null;
+ try {
+ return
MarshallingContext.DEFAULT_SESSION.convertToType(value, type);
+ } catch (InvalidDataConversionException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the raw backing value wrapped by this node.
+ *
+ * @return The raw backing value. Can be <jk>null</jk>.
+ */
+ public Object value() {
+ return value;
+ }
+
+ /**
+ * Returns the child node for the specified key on an object node.
+ *
+ * @param name The key.
+ * @return The child node, or <jk>null</jk> if this is not an object
node or the key is absent.
+ */
+ public MarshalledNode get(String name) {
+ if (value instanceof Map<?,?> value2 &&
value2.containsKey(name))
+ return new MarshalledNode(value2.get(name));
+ return null;
+ }
+
+ /**
+ * Returns the element node at the specified index on an array node.
+ *
+ * @param index The zero-based index.
+ * @return The element node, or <jk>null</jk> if this is not an array
node or the index is out of range.
+ */
+ public MarshalledNode get(int index) {
+ if (value instanceof List<?> value2 && index >= 0 && index <
value2.size())
+ return new MarshalledNode(value2.get(index));
+ return null;
+ }
+
+ /**
+ * Returns the number of entries (object) or elements (array) in this
node.
+ *
+ * @return The size, or <c>0</c> if this is a value or null node.
+ */
+ public int size() {
+ if (value instanceof Map<?,?> value2)
+ return value2.size();
+ if (value instanceof List<?> value2)
+ return value2.size();
+ return 0;
+ }
+
+ /**
+ * Adds or replaces an entry on an object node, mutating the backing
{@link Map} in place.
+ *
+ * @param name The key.
+ * @param value The value.
+ * @return This object.
+ * @throws IllegalStateException If this is not an object node.
+ */
+ @SuppressWarnings({
+ "unchecked" // Backing map of an object node always accepts
String keys / Object values.
+ })
+ public MarshalledNode put(String name, Object value) {
+ if (! isObject())
+ throw illegalState("put(String,Object) is only valid on
an object node.");
+ ((Map<String,Object>)this.value).put(name, value);
+ return this;
+ }
+
+ /**
+ * Appends a value on an array node, mutating the backing {@link List}
in place.
+ *
+ * @param value The value to append.
+ * @return This object.
+ * @throws IllegalStateException If this is not an array node.
+ */
+ @SuppressWarnings({
+ "unchecked" // Backing list of an array node always accepts
Object elements.
+ })
+ public MarshalledNode add(Object value) {
+ if (! isArray())
+ throw illegalState("add(Object) is only valid on an
array node.");
+ ((List<Object>)this.value).add(value);
+ return this;
+ }
+
+ /**
+ * Returns a new node backed by a deep copy of this node's backing
value.
+ *
+ * <p>
+ * Maps are copied into new {@link JsonMap} instances and lists into
new {@link JsonList} instances (recursively);
+ * scalars and <jk>null</jk> are returned as-is (they are immutable).
Mutating the copy does not affect the original
+ * and vice versa.
+ *
+ * @return A new independent node, never <jk>null</jk>.
+ */
+ public MarshalledNode copy() {
+ return new MarshalledNode(deepCopy(value));
+ }
+
+ /**
+ * Returns the node addressed by the specified <a class="doclink"
href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a>
+ * JSON Pointer relative to this node's backing value.
+ *
+ * <p>
+ * A read-miss (missing key, out-of-range index, or type mismatch)
returns <jk>null</jk> so callers can null-check.
+ * If the addressed value exists and is JSON <jk>null</jk>, a null node
is returned (use {@link #isNull()} to test it).
+ * The empty pointer addresses this node's whole backing value.
+ *
+ * @param pointer The RFC 6901 JSON Pointer.
+ * @return A node wrapping the addressed value, or <jk>null</jk> if the
addressed value is absent.
+ */
+ public MarshalledNode at(String pointer) {
+ return find(pointer).orElse(null);
+ }
+
+ /**
+ * Optional read variant of {@link #at(String)}.
+ *
+ * @param pointer The RFC 6901 JSON Pointer.
+ * @return The addressed node (present, possibly a null node), or an
empty {@link Optional} if the addressed value is absent.
+ */
+ public Optional<MarshalledNode> find(String pointer) {
+ var o = JsonPointer.of(pointer).evalOrMissing(value);
+ return JsonPointer.isMissing(o) ? Optional.empty() :
Optional.of(new MarshalledNode(o));
+ }
+
+ /**
+ * Writes the specified value at the location addressed by the specified
+ * <a class="doclink"
href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> JSON Pointer,
mutating the
+ * backing container in place and auto-vivifying missing intermediate
containers.
+ *
+ * <p>
+ * Intermediate containers are created based on the next token (a
numeric or <c>-</c> token implies a {@link JsonList},
+ * otherwise a {@link JsonMap}); the <c>-</c> token on a list appends.
Because the backing value is <jk>final</jk>,
+ * the empty (root) pointer cannot replace this node's whole document
in place and is therefore rejected.
+ *
+ * @param pointer The RFC 6901 JSON Pointer. Must not be the root
pointer.
+ * @param value The value to set.
+ * @return This object.
+ * @throws IllegalArgumentException If <c>pointer</c> is the empty/root
pointer.
+ */
+ public MarshalledNode set(String pointer, Object value) {
+ var p = JsonPointer.of(pointer);
+ if (p.isRoot())
+ throw illegalArg("Cannot set the whole document via the
root pointer on a node.");
+ p.set(this.value, value);
+ return this;
+ }
+
+ /**
+ * Removes the member or element addressed by the specified
+ * <a class="doclink"
href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> JSON Pointer.
+ *
+ * @param pointer The RFC 6901 JSON Pointer.
+ * @return The removed value, or <jk>null</jk> if the addressed
member/element was absent.
+ */
+ public Object remove(String pointer) {
+ return JsonPointer.of(pointer).remove(value);
+ }
+
+ private <T> T asScalar(Class<T> type) {
+ if (! isValue())
+ return null;
+ try {
+ return
MarshallingContext.DEFAULT_SESSION.convertToType(value, type);
+ } catch (InvalidDataConversionException e) {
+ return null;
+ }
+ }
+
+ private static Object deepCopy(Object v) {
+ if (v instanceof Map<?,?> v2) {
+ var r = new JsonMap();
+ v2.forEach((k, val) -> r.put(String.valueOf(k),
deepCopy(val)));
+ return r;
+ }
+ if (v instanceof List<?> v2) {
+ var r = new JsonList();
+ v2.forEach(e -> r.add(deepCopy(e)));
+ return r;
+ }
+ return v;
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/package-info.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/package-info.java
index 44c84c56a6..4fd7a65af6 100755
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/package-info.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/collections/package-info.java
@@ -21,7 +21,7 @@
* <ul>
* <li>{@link org.apache.juneau.marshall.collections.MarshalledMap} /
{@link org.apache.juneau.marshall.collections.MarshalledList}
* — neutral, marshaller-agnostic base classes carrying
typed accessors, fluent setters,
- * bean integration, and {@link
org.apache.juneau.marshall.objecttools.ObjectRest}-based path navigation.
+ * bean integration, and {@link
org.apache.juneau.marshall.objecttools.PathTraversal}-based path navigation.
* Their default {@link java.lang.Object#toString()} is the
inherited {@link java.util.LinkedHashMap}
* / {@link java.util.LinkedList} form.
* <li>{@link org.apache.juneau.marshall.collections.JsonMap} / {@link
org.apache.juneau.marshall.collections.JsonList}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/ObjectRest.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/PathTraversal.java
similarity index 81%
rename from
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/ObjectRest.java
rename to
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/PathTraversal.java
index d8718392fe..a5872b0ec2 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/ObjectRest.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/PathTraversal.java
@@ -20,21 +20,22 @@ import static java.net.HttpURLConnection.*;
import static org.apache.juneau.commons.utils.StringUtils.*;
import static org.apache.juneau.commons.utils.Utils.*;
-import java.io.*;
import java.lang.reflect.*;
import java.util.*;
-import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.marshall.*;
import org.apache.juneau.marshall.collections.*;
-import org.apache.juneau.marshall.json5.*;
import org.apache.juneau.marshall.parser.*;
/**
- * POJO REST API.
+ * Traversal engine for performing standard REST operations (GET, PUT, POST,
DELETE) against nodes in a POJO model.
+ *
+ * <p>
+ * <b>Internal API:</b> This class is the relocated traversal engine behind
the {@code Marshalled*} path-addressing
+ * methods and the Swagger/OpenApi {@code $ref} resolver. It is public only
for cross-module access and is <b>not</b> a
+ * supported public API — do not use it directly.
*
* <p>
- * Provides the ability to perform standard REST operations (GET, PUT, POST,
DELETE) against nodes in a POJO model.
* Nodes in the POJO model are addressed using URLs.
*
* <p>
@@ -84,8 +85,8 @@ import org.apache.juneau.marshall.parser.*;
* + <js>"} "</js>
* );
*
- * <jc>// Wrap Map inside an ObjectRest object</jc>
- * ObjectRest <jv>johnSmith</jv> =
ObjectRest.<jsm>create</jsm>(<jv>map</jv>);
+ * <jc>// Wrap Map inside a PathTraversal object</jc>
+ * PathTraversal <jv>johnSmith</jv> =
PathTraversal.<jsm>create</jsm>(<jv>map</jv>);
*
* <jc>// Get a simple value at the top level</jc>
* <jc>// "John Smith"</jc>
@@ -130,14 +131,14 @@ import org.apache.juneau.marshall.parser.*;
* <h5 class='section'>Example:</h5>
* <p class='bjava'>
* <jc>// Get map/bean with name attribute value of 'foo' from a list of
items</jc>
- * Map <jv>map</jv> =
<jv>objectRest</jv>.getMap(<js>"/items/@name=foo"</js>);
+ * Map <jv>map</jv> =
<jv>pathTraversal</jv>.getMap(<js>"/items/@name=foo"</js>);
* </p>
*/
@SuppressWarnings({
"unchecked", // Type erasure requires unchecked operations for generic
collections
"rawtypes" // Raw types necessary for generic type handling
})
-public class ObjectRest {
+public class PathTraversal {
class JsonNode {
Object o;
ClassMeta cm;
@@ -167,20 +168,20 @@ public class ObjectRest {
/**
* Static creator.
* @param o The object being wrapped.
- * @return A new {@link ObjectRest} object.
+ * @return A new {@link PathTraversal} object.
*/
- public static ObjectRest create(Object o) {
- return new ObjectRest(o);
+ public static PathTraversal create(Object o) {
+ return new PathTraversal(o);
}
/**
* Static creator.
* @param o The object being wrapped.
* @param parser The parser to use for parsing arguments and converting
objects to the correct data type.
- * @return A new {@link ObjectRest} object.
+ * @return A new {@link PathTraversal} object.
*/
- public static ObjectRest create(Object o, ReaderParser parser) {
- return new ObjectRest(o, parser);
+ public static PathTraversal create(Object o, ReaderParser parser) {
+ return new PathTraversal(o, parser);
}
/** Handle nulls and strip off leading '/' char. */
@@ -201,7 +202,7 @@ public class ObjectRest {
try {
return Integer.parseInt(key);
} catch (@SuppressWarnings("unused") NumberFormatException e) {
- throw new ObjectRestException(HTTP_BAD_REQUEST, "Cannot
address an item in an array with a non-integer key ''{0}''", key);
+ throw new PathTraversalException(HTTP_BAD_REQUEST,
"Cannot address an item in an array with a non-integer key ''{0}''", key);
}
}
@@ -214,8 +215,6 @@ public class ObjectRest {
return a2;
}
- private ReaderParser parser = Json5Parser.DEFAULT;
-
final MarshallingSession session;
/** If true, the root cannot be overwritten */
@@ -232,7 +231,7 @@ public class ObjectRest {
*
* @param o The object to be wrapped.
*/
- public ObjectRest(Object o) {
+ public PathTraversal(Object o) {
this(o, null);
}
@@ -245,11 +244,8 @@ public class ObjectRest {
* @param o The object to be wrapped.
* @param parser The parser to use for parsing arguments and converting
objects to the correct data type.
*/
- public ObjectRest(Object o, ReaderParser parser) {
+ public PathTraversal(Object o, ReaderParser parser) {
this.session = parser == null ?
MarshallingContext.DEFAULT_SESSION :
parser.getMarshallingContext().getSession();
- if (parser == null)
- parser = Json5Parser.DEFAULT;
- this.parser = parser;
this.root = new JsonNode(null, null, o, session.object());
}
@@ -288,22 +284,22 @@ public class ObjectRest {
*
* <h5 class='section'>Examples:</h5>
* <p class='bjava'>
- * ObjectRest <jv>objectRest</jv> = <jk>new</jk>
ObjectRest(<jv>object</jv>);
+ * PathTraversal <jv>pathTraversal</jv> = <jk>new</jk>
PathTraversal(<jv>object</jv>);
*
* <jc>// Value converted to a string.</jc>
- * String <jv>string</jv> =
<jv>objectRest</jv>.get(<js>"path/to/string"</js>, String.<jk>class</jk>);
+ * String <jv>string</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/string"</js>, String.<jk>class</jk>);
*
* <jc>// Value converted to a bean.</jc>
- * MyBean <jv>bean</jv> =
<jv>objectRest</jv>.get(<js>"path/to/bean"</js>, MyBean.<jk>class</jk>);
+ * MyBean <jv>bean</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/bean"</js>, MyBean.<jk>class</jk>);
*
* <jc>// Value converted to a bean array.</jc>
- * MyBean[] <jv>beanArray</jv> =
<jv>objectRest</jv>.get(<js>"path/to/beanarray"</js>, MyBean[].<jk>class</jk>);
+ * MyBean[] <jv>beanArray</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/beanarray"</js>,
MyBean[].<jk>class</jk>);
*
* <jc>// Value converted to a linked-list of objects.</jc>
- * List <jv>list</jv> =
<jv>objectRest</jv>.get(<js>"path/to/list"</js>, LinkedList.<jk>class</jk>);
+ * List <jv>list</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/list"</js>, LinkedList.<jk>class</jk>);
*
* <jc>// Value converted to a map of object keys/values.</jc>
- * Map <jv>map</jv> =
<jv>objectRest</jv>.get(<js>"path/to/map"</js>, TreeMap.<jk>class</jk>);
+ * Map <jv>map</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/map"</js>, TreeMap.<jk>class</jk>);
* </p>
*
* @param url
@@ -329,22 +325,22 @@ public class ObjectRest {
*
* <h5 class='section'>Examples:</h5>
* <p class='bjava'>
- * ObjectRest <jv>objectRest</jv> = <jk>new</jk>
ObjectRest(<jv>object</jv>);
+ * PathTraversal <jv>pathTraversal</jv> = <jk>new</jk>
PathTraversal(<jv>object</jv>);
*
* <jc>// Value converted to a linked-list of strings.</jc>
- * List<String> <jv>list1</jv> =
<jv>objectRest</jv>.get(<js>"path/to/list1"</js>, LinkedList.<jk>class</jk>,
String.<jk>class</jk>);
+ * List<String> <jv>list1</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/list1"</js>, LinkedList.<jk>class</jk>,
String.<jk>class</jk>);
*
* <jc>// Value converted to a linked-list of beans.</jc>
- * List<MyBean> <jv>list2</jv> =
<jv>objectRest</jv>.get(<js>"path/to/list2"</js>, LinkedList.<jk>class</jk>,
MyBean.<jk>class</jk>);
+ * List<MyBean> <jv>list2</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/list2"</js>, LinkedList.<jk>class</jk>,
MyBean.<jk>class</jk>);
*
* <jc>// Value converted to a linked-list of linked-lists of
strings.</jc>
- * List<List<String>> <jv>list3</jv> =
<jv>objectRest</jv>.get(<js>"path/to/list3"</js>, LinkedList.<jk>class</jk>,
LinkedList.<jk>class</jk>, String.<jk>class</jk>);
+ * List<List<String>> <jv>list3</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/list3"</js>, LinkedList.<jk>class</jk>,
LinkedList.<jk>class</jk>, String.<jk>class</jk>);
*
* <jc>// Value converted to a map of string keys/values.</jc>
- * Map<String,String> <jv>map1</jv> =
<jv>objectRest</jv>.get(<js>"path/to/map1"</js>, TreeMap.<jk>class</jk>,
String.<jk>class</jk>, String.<jk>class</jk>);
+ * Map<String,String> <jv>map1</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/map1"</js>, TreeMap.<jk>class</jk>,
String.<jk>class</jk>, String.<jk>class</jk>);
*
* <jc>// Value converted to a map containing string keys and
values of lists containing beans.</jc>
- * Map<String,List<MyBean>> <jv>map2</jv> =
<jv>objectRest</jv>.get(<js>"path/to/map2"</js>, TreeMap.<jk>class</jk>,
String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
+ * Map<String,List<MyBean>> <jv>map2</jv> =
<jv>pathTraversal</jv>.get(<js>"path/to/map2"</js>, TreeMap.<jk>class</jk>,
String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
* </p>
*
* <p>
@@ -602,28 +598,6 @@ public class ObjectRest {
return getWithDefault(url, defVal, Map.class);
}
- /**
- * Returns the list of available methods that can be passed to the
{@link #invokeMethod(String, String, String)}
- * for the object addressed by the specified URL.
- *
- * @param url The URL.
- * @return The list of methods.
- */
- @SuppressWarnings({
- "java:S1168" // null when object not found at URL.
- })
- public Collection<String> getPublicMethods(String url) {
- var o = get(url);
- if (o == null)
- return null;
- return session
- .getClassMeta(o.getClass())
- .getPublicMethods()
- .stream()
- .map(x -> x.getSignature())
- .toList();
- }
-
/**
* The root object that was passed into the constructor of this method.
*
@@ -711,41 +685,6 @@ public class ObjectRest {
return session.convertToType(o, type, args);
}
- /**
- * Executes the specified method with the specified parameters on the
specified object.
- *
- * @param url The URL of the element to retrieve.
- * @param method
- * The method signature.
- * <p>
- * Can be any of the following formats:
- * <ul class='spaced-list'>
- * <li>
- * Method name only. e.g. <js>"myMethod"</js>.
- * <li>
- * Method name with class names. e.g.
<js>"myMethod(String,int)"</js>.
- * <li>
- * Method name with fully-qualified class names.
e.g. <js>"myMethod(java.util.String,int)"</js>.
- * </ul>
- * <p>
- * As a rule, use the simplest format needed to uniquely resolve a
method.
- * @param args
- * The arguments to pass as parameters to the method.
- * These will automatically be converted to the appropriate object
type if possible.
- * This must be an array, like a JSON array.
- * @return The returned object from the method call.
- * @throws ExecutableException Exception occurred on invoked
constructor/method/field.
- * @throws ParseException Malformed input encountered.
- * @throws IOException Thrown by underlying stream.
- */
- public Object invokeMethod(String url, String method, String args)
throws ExecutableException, ParseException, IOException {
- try {
- return new ObjectIntrospector(get(url),
parser).invokeMethod(method, args);
- } catch (NoSuchMethodException | IllegalArgumentException |
InvocationTargetException | IllegalAccessException e) {
- throw new ExecutableException(e);
- }
- }
-
/**
* Adds a value to a list element in a POJO model.
*
@@ -799,7 +738,7 @@ public class ObjectRest {
*
* @return This object.
*/
- public ObjectRest setRootLocked() {
+ public PathTraversal setRootLocked() {
this.rootLocked = true;
return this;
}
@@ -833,7 +772,7 @@ public class ObjectRest {
"java:S3776", // Cognitive complexity acceptable for REST
operation routing logic
"java:S6541" // Cognitive complexity acceptable for REST
operation routing logic
})
- private Object service(int method, String url, Object val) throws
ObjectRestException {
+ private Object service(int method, String url, Object val) throws
PathTraversalException {
url = normalizeUrl(url);
@@ -850,16 +789,16 @@ public class ObjectRest {
if (method == PUT) {
if (url.isEmpty()) {
if (rootLocked)
- throw new
ObjectRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
+ throw new
PathTraversalException(HTTP_FORBIDDEN, "Cannot overwrite root object");
var o = root.o;
root = new JsonNode(null, null, val,
session.object());
return o;
}
var n = (parentUrl == null ? root : getNode(parentUrl,
root));
if (n == null)
- throw new ObjectRestException(HTTP_NOT_FOUND,
"Node at URL ''{0}'' not found.", parentUrl);
+ throw new
PathTraversalException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.",
parentUrl);
if (n.o == null && n.cm.isObject())
- throw new ObjectRestException(HTTP_NOT_FOUND,
"Node at URL ''{0}'' not found.", parentUrl);
+ throw new
PathTraversalException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.",
parentUrl);
var cm = n.cm;
var o = n.o;
if (cm.isMap())
@@ -867,7 +806,7 @@ public class ObjectRest {
if (cm.isCollection() && o instanceof List o2)
return o2.set(parseInt(childKey), convert(val,
cm.getElementType()));
if (cm.isCollection())
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot PUT to indexed position in non-List collection ''{0}'' of type
''{1}''", url, cm);
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Cannot PUT to indexed position in
non-List collection ''{0}'' of type ''{1}''", url, cm);
if (cm.isArray()) {
o = setArrayEntry(n.o, parseInt(childKey), val,
cm.getElementType());
var pct = n.parent.cm;
@@ -881,11 +820,11 @@ public class ObjectRest {
m.put(n.keyName, o);
return url;
}
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot perform PUT on ''{0}'' with parent node type ''{1}''", url, pct);
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' with
parent node type ''{1}''", url, pct);
}
if (cm.isBean())
return session.toBeanMap(o).put(childKey, val);
- throw new ObjectRestException(HTTP_BAD_REQUEST, "Cannot
perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
+ throw new PathTraversalException(HTTP_BAD_REQUEST,
"Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
}
if (method == POST) {
@@ -903,13 +842,13 @@ public class ObjectRest {
root = new JsonNode(null, null, o2,
null);
return url + "/" + (o2.length - 1);
}
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of
type ''{1}''", url, cm);
}
var n = getNode(url, root);
if (n == null)
- throw new ObjectRestException(HTTP_NOT_FOUND,
"Node at URL ''{0}'' not found.", url);
+ throw new
PathTraversalException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", url);
if (n.o == null && n.cm.isObject())
- throw new ObjectRestException(HTTP_NOT_FOUND,
"Node at URL ''{0}'' not found.", url);
+ throw new
PathTraversalException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", url);
var cm = n.cm;
var o = n.o;
if (cm.isArray()) {
@@ -925,29 +864,29 @@ public class ObjectRest {
m.put(childKey, o2);
return url + "/" + (o2.length - 1);
}
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with
parent node type ''{1}''", url, pct);
}
if (cm.isCollection()) {
var c = (Collection)o;
c.add(convert(val, cm.getElementType()));
return (c instanceof List c2 ? url + "/" +
(c2.size() - 1) : null);
}
- throw new ObjectRestException(HTTP_BAD_REQUEST, "Cannot
perform POST on ''{0}'' of type ''{1}''", url, cm);
+ throw new PathTraversalException(HTTP_BAD_REQUEST,
"Cannot perform POST on ''{0}'' of type ''{1}''", url, cm);
}
if (method == DELETE) {
if (url.isEmpty()) {
if (rootLocked)
- throw new
ObjectRestException(HTTP_FORBIDDEN, "Cannot overwrite root object");
+ throw new
PathTraversalException(HTTP_FORBIDDEN, "Cannot overwrite root object");
var o = root.o;
root = new JsonNode(null, null, null,
session.object());
return o;
}
var n = (parentUrl == null ? root : getNode(parentUrl,
root));
if (n == null)
- throw new ObjectRestException(HTTP_NOT_FOUND,
"Node at URL ''{0}'' not found.", parentUrl);
+ throw new
PathTraversalException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.",
parentUrl);
if (n.o == null && n.cm.isObject())
- throw new ObjectRestException(HTTP_NOT_FOUND,
"Node at URL ''{0}'' not found.", parentUrl);
+ throw new
PathTraversalException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.",
parentUrl);
var cm = n.cm;
var o = n.o;
if (cm.isMap())
@@ -955,7 +894,7 @@ public class ObjectRest {
if (cm.isCollection() && o instanceof List o2)
return o2.remove(parseInt(childKey));
if (cm.isCollection())
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot DELETE indexed position in non-List collection ''{0}'' of type
''{1}''", url, cm);
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Cannot DELETE indexed position in
non-List collection ''{0}'' of type ''{1}''", url, cm);
if (cm.isArray()) {
int index = parseInt(childKey);
var old = ((Object[])o)[index];
@@ -971,11 +910,11 @@ public class ObjectRest {
m.put(n.keyName, o2);
return old;
}
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct);
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with
parent node type ''{1}''", url, pct);
}
if (cm.isBean())
return session.toBeanMap(o).put(childKey, null);
- throw new ObjectRestException(HTTP_BAD_REQUEST, "Cannot
perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
+ throw new PathTraversalException(HTTP_BAD_REQUEST,
"Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm);
}
return null; // Never gets here.
@@ -1039,7 +978,7 @@ public class ObjectRest {
o2 = m.get(parentKey);
var pMeta = m.getPropertyMeta(parentKey);
if (pMeta == null)
- throw new ObjectRestException(HTTP_BAD_REQUEST,
"Unknown property ''{0}'' encountered while trying to parse into class
''{1}''", parentKey, m.getBeanInfo());
+ throw new
PathTraversalException(HTTP_BAD_REQUEST, "Unknown property ''{0}'' encountered
while trying to parse into class ''{1}''", parentKey, m.getBeanInfo());
ct2 = (ClassMeta) pMeta.getBeanInfo();
}
@@ -1048,4 +987,4 @@ public class ObjectRest {
return getNode(childUrl, new JsonNode(n, parentKey, o2, ct2));
}
-}
\ No newline at end of file
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/ObjectRestException.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/PathTraversalException.java
similarity index 87%
rename from
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/ObjectRestException.java
rename to
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/PathTraversalException.java
index 26a293ccdf..8ca231399c 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/ObjectRestException.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/objecttools/PathTraversalException.java
@@ -22,7 +22,7 @@ import java.text.*;
import org.apache.juneau.marshall.*;
/**
- * Generic exception thrown from the {@link ObjectRest} class.
+ * Generic exception thrown from the {@link PathTraversal} class.
*
* <p>
* Typically, this is a user-error, such as trying to address a non-existent
node in the tree.
@@ -48,7 +48,7 @@ import org.apache.juneau.marshall.*;
"java:S110", // Deep inheritance inherent to the exception hierarchy
"java:S2166" // False positive: this IS an exception
(BasicRuntimeException -> RuntimeException); name is correct public API.
})
-public class ObjectRestException extends BasicRuntimeException {
+public class PathTraversalException extends BasicRuntimeException {
private static final long serialVersionUID = 1L;
@@ -61,7 +61,7 @@ public class ObjectRestException extends
BasicRuntimeException {
* @param message The detailed message.
* @param args Optional {@link MessageFormat}-style arguments.
*/
- public ObjectRestException(int status, String message, Object...args) {
+ public PathTraversalException(int status, String message,
Object...args) {
this(null, status, message, args);
}
@@ -73,7 +73,7 @@ public class ObjectRestException extends
BasicRuntimeException {
* @param message The {@link MessageFormat}-style message.
* @param args Optional {@link MessageFormat}-style arguments.
*/
- public ObjectRestException(Throwable cause, int status, String message,
Object...args) {
+ public PathTraversalException(Throwable cause, int status, String
message, Object...args) {
super(cause, message, args);
this.status = status;
}
@@ -89,8 +89,8 @@ public class ObjectRestException extends
BasicRuntimeException {
public int getStatus() { return status; }
@Override /* Overridden from BasicRuntimeException */
- public ObjectRestException setMessage(String message, Object...args) {
+ public PathTraversalException setMessage(String message, Object...args)
{
super.setMessage(message, args);
return this;
}
-}
\ No newline at end of file
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/JsonMap_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/JsonMap_Test.java
index a648ca0899..877e155744 100755
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/JsonMap_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/JsonMap_Test.java
@@ -26,7 +26,6 @@ import java.util.*;
import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.marshall.json.*;
import org.apache.juneau.marshall.json5.*;
-import org.apache.juneau.marshall.objecttools.*;
import org.junit.jupiter.api.*;
@SuppressWarnings({
@@ -244,13 +243,11 @@ class JsonMap_Test extends TestBase {
}
private static String getDeepString(Json5Map m, String url) {
- var r = ObjectRest.create(m);
- return (String)r.get(url);
+ return m.getAt(url, String.class);
}
private static Boolean getDeepBoolean(Json5Map m, String url) {
- var r = ObjectRest.create(m);
- return (Boolean)r.get(url);
+ return m.getAt(url, Boolean.class);
}
private static void checkStep(int step, String input, String output,
String expectedValue) {
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/collections/JsonPointer_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/collections/JsonPointer_Test.java
new file mode 100644
index 0000000000..c6bb2bfe3a
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/collections/JsonPointer_Test.java
@@ -0,0 +1,227 @@
+/*
+ * 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.juneau.marshall.collections;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for the RFC 6901 JSON-Pointer implementation {@link JsonPointer}.
+ *
+ * <p>
+ * Covers parsing/validation, the RFC 6901 §5 example vectors, {@code
~0}/{@code ~1} token escaping,
+ * nested read navigation, auto-vivifying writes (including the {@code -}
append token), and removal.
+ */
+class JsonPointer_Test extends TestBase {
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a - Parsing / validation / toString
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_emptyPointerIsRoot() {
+ assertEquals("", JsonPointer.of("").toString());
+ }
+
+ @Test void a02_toStringRoundTrip() {
+ assertEquals("/foo/0", JsonPointer.of("/foo/0").toString());
+ assertEquals("/a~1b", JsonPointer.of("/a~1b").toString());
+ assertEquals("/m~0n", JsonPointer.of("/m~0n").toString());
+ assertEquals("/", JsonPointer.of("/").toString());
+ }
+
+ @Test void a03_malformedPointerThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
JsonPointer.of("foo"));
+ assertThrows(IllegalArgumentException.class, () ->
JsonPointer.of("foo/bar"));
+ assertThrows(IllegalArgumentException.class, () ->
JsonPointer.of(null));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // b - Token escape / unescape
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_decodeToken() {
+ assertEquals("a/b", JsonPointer.decodeToken("a~1b"));
+ assertEquals("m~n", JsonPointer.decodeToken("m~0n"));
+ // ~01 decodes to ~1 (the ~0 -> ~ is applied after ~1 -> /).
+ assertEquals("~1", JsonPointer.decodeToken("~01"));
+ assertEquals("plain", JsonPointer.decodeToken("plain"));
+ }
+
+ @Test void b02_encodeToken() {
+ assertEquals("a~1b", JsonPointer.encodeToken("a/b"));
+ assertEquals("m~0n", JsonPointer.encodeToken("m~n"));
+ assertEquals("~01", JsonPointer.encodeToken("~1"));
+ assertEquals("plain", JsonPointer.encodeToken("plain"));
+ }
+
+ @Test void b03_escapeRoundTrip() {
+ for (var raw : new String[]{"a/b", "m~n", "~", "/", "a~1b",
"c/d~e"})
+ assertEquals(raw,
JsonPointer.decodeToken(JsonPointer.encodeToken(raw)));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // c - RFC 6901 §5 example vectors
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ private static JsonMap rfcDoc() {
+ return JsonMap.of(
+ "foo", JsonList.of("bar", "baz"),
+ "", 0,
+ "a/b", 1,
+ "c%d", 2,
+ "e^f", 3,
+ "g|h", 4,
+ "i\\j", 5,
+ "k\"l", 6,
+ " ", 7,
+ "m~n", 8
+ );
+ }
+
+ @Test void c01_wholeDocument() {
+ var doc = rfcDoc();
+ assertSame(doc, JsonPointer.of("").eval(doc));
+ }
+
+ @Test void c02_array() {
+ var doc = rfcDoc();
+ assertEquals(JsonList.of("bar", "baz"),
JsonPointer.of("/foo").eval(doc));
+ }
+
+ @Test void c03_arrayIndex() {
+ assertEquals("bar", JsonPointer.of("/foo/0").eval(rfcDoc()));
+ }
+
+ @Test void c04_emptyStringKey() {
+ assertEquals(0, JsonPointer.of("/").eval(rfcDoc()));
+ }
+
+ @Test void c05_escapedSlash() {
+ assertEquals(1, JsonPointer.of("/a~1b").eval(rfcDoc()));
+ }
+
+ @Test void c06_percent() {
+ assertEquals(2, JsonPointer.of("/c%d").eval(rfcDoc()));
+ }
+
+ @Test void c07_caret() {
+ assertEquals(3, JsonPointer.of("/e^f").eval(rfcDoc()));
+ }
+
+ @Test void c08_pipe() {
+ assertEquals(4, JsonPointer.of("/g|h").eval(rfcDoc()));
+ }
+
+ @Test void c09_backslash() {
+ assertEquals(5, JsonPointer.of("/i\\j").eval(rfcDoc()));
+ }
+
+ @Test void c10_quote() {
+ assertEquals(6, JsonPointer.of("/k\"l").eval(rfcDoc()));
+ }
+
+ @Test void c11_space() {
+ assertEquals(7, JsonPointer.of("/ ").eval(rfcDoc()));
+ }
+
+ @Test void c12_escapedTilde() {
+ assertEquals(8, JsonPointer.of("/m~0n").eval(rfcDoc()));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // d - Read navigation + miss behavior
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void d01_nested() {
+ var doc = JsonMap.of("a", JsonMap.of("b", JsonList.of("x", "y",
"z")));
+ assertEquals("z", JsonPointer.of("/a/b/2").eval(doc));
+ }
+
+ @Test void d02_missingKey() {
+ assertNull(JsonPointer.of("/missing").eval(JsonMap.of("a", 1)));
+ assertNull(JsonPointer.of("/a/missing").eval(JsonMap.of("a",
JsonMap.of("b", 1))));
+ }
+
+ @Test void d03_indexOutOfRange() {
+ assertNull(JsonPointer.of("/0").eval(JsonList.of()));
+ assertNull(JsonPointer.of("/5").eval(JsonList.of("x")));
+ }
+
+ @Test void d04_typeMismatch() {
+ // Descending into a scalar yields a miss.
+ assertNull(JsonPointer.of("/a/b").eval(JsonMap.of("a",
"scalar")));
+ // Non-numeric token on an array yields a miss.
+ assertNull(JsonPointer.of("/x").eval(JsonList.of(1, 2)));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // e - Write (set) + auto-vivify + append
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_replaceExisting() {
+ var root = JsonMap.of("a", 1);
+ assertSame(root, JsonPointer.of("/a").set(root, 2));
+ assertEquals(2, JsonPointer.of("/a").eval(root));
+ }
+
+ @Test void e02_vivifyNestedObject() {
+ var root = new JsonMap();
+ JsonPointer.of("/a/b/c").set(root, 1);
+ assertEquals("{\"a\":{\"b\":{\"c\":1}}}", Json.of(root));
+ }
+
+ @Test void e03_vivifyNestedArray() {
+ var root = new JsonMap();
+ JsonPointer.of("/a/0").set(root, "x");
+ assertEquals("{\"a\":[\"x\"]}", Json.of(root));
+ assertInstanceOf(JsonList.class, root.get("a"));
+ }
+
+ @Test void e04_appendToken() {
+ var root = JsonMap.of("a", JsonList.of(1, 2));
+ JsonPointer.of("/a/-").set(root, 3);
+ assertEquals("{\"a\":[1,2,3]}", Json.of(root));
+ }
+
+ @Test void e05_rootPointerReturnsValue() {
+ assertEquals("v", JsonPointer.of("").set(JsonMap.of("a", 1),
"v"));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // f - Remove
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void f01_removeMember() {
+ var root = JsonMap.of("a", 1, "b", 2);
+ assertEquals(1, JsonPointer.of("/a").remove(root));
+ assertEquals("{\"b\":2}", Json.of(root));
+ }
+
+ @Test void f02_removeArrayElement() {
+ var root = JsonMap.of("a", JsonList.of("x", "y", "z"));
+ assertEquals("y", JsonPointer.of("/a/1").remove(root));
+ assertEquals("{\"a\":[\"x\",\"z\"]}", Json.of(root));
+ }
+
+ @Test void f03_removeAbsentReturnsNull() {
+ assertNull(JsonPointer.of("/missing").remove(JsonMap.of("a",
1)));
+ assertNull(JsonPointer.of("/a/5").remove(JsonMap.of("a",
JsonList.of("x"))));
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/collections/MarshalledNode_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/collections/MarshalledNode_Test.java
new file mode 100644
index 0000000000..7b82ab49ca
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/collections/MarshalledNode_Test.java
@@ -0,0 +1,346 @@
+/*
+ * 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.juneau.marshall.collections;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.marshaller.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Tests for the typed tree façade {@link MarshalledNode}.
+ *
+ * <p>
+ * Covers node-type predicates, typed accessors, child navigation, fluent tree
building (asserting both the
+ * underlying {@link JsonMap}/{@link JsonList} structure and the serialized
JSON output), the live-view mutation
+ * contract, and {@link MarshalledNode#copy()} deep-copy independence.
+ */
+class MarshalledNode_Test extends TestBase {
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // a - Predicates
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_objectNodePredicates() {
+ var n = MarshalledNode.of(JsonMap.of("a", 1));
+ assertTrue(n.isObject());
+ assertFalse(n.isArray());
+ assertFalse(n.isValue());
+ assertFalse(n.isNull());
+ }
+
+ @Test void a02_arrayNodePredicates() {
+ var n = MarshalledNode.of(JsonList.of(1, 2, 3));
+ assertFalse(n.isObject());
+ assertTrue(n.isArray());
+ assertFalse(n.isValue());
+ assertFalse(n.isNull());
+ }
+
+ @Test void a03_valueNodePredicates() {
+ var n = MarshalledNode.of("hello");
+ assertFalse(n.isObject());
+ assertFalse(n.isArray());
+ assertTrue(n.isValue());
+ assertFalse(n.isNull());
+ }
+
+ @Test void a04_nullNodePredicates() {
+ var n = MarshalledNode.of(null);
+ assertFalse(n.isObject());
+ assertFalse(n.isArray());
+ assertFalse(n.isValue());
+ assertTrue(n.isNull());
+ }
+
+ @Test void a05_objectNodeFactory() {
+ var n = MarshalledNode.objectNode();
+ assertTrue(n.isObject());
+ assertInstanceOf(JsonMap.class, n.value());
+ assertEquals(0, n.size());
+ }
+
+ @Test void a06_arrayNodeFactory() {
+ var n = MarshalledNode.arrayNode();
+ assertTrue(n.isArray());
+ assertInstanceOf(JsonList.class, n.value());
+ assertEquals(0, n.size());
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // b - Typed accessors
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_asString() {
+ assertEquals("hello", MarshalledNode.of("hello").asString());
+ assertEquals("123", MarshalledNode.of(123).asString());
+ }
+
+ @Test void b02_asInt() {
+ assertEquals(Integer.valueOf(42),
MarshalledNode.of(42).asInt());
+ assertEquals(Integer.valueOf(7),
MarshalledNode.of("7").asInt());
+ }
+
+ @Test void b03_asLong() {
+ assertEquals(Long.valueOf(9999999999L),
MarshalledNode.of(9999999999L).asLong());
+ assertEquals(Long.valueOf(5), MarshalledNode.of("5").asLong());
+ }
+
+ @Test void b04_asDouble() {
+ assertEquals(Double.valueOf(3.5),
MarshalledNode.of(3.5).asDouble());
+ assertEquals(Double.valueOf(2.0),
MarshalledNode.of("2.0").asDouble());
+ }
+
+ @Test void b05_asBoolean() {
+ assertEquals(Boolean.TRUE, MarshalledNode.of(true).asBoolean());
+ assertEquals(Boolean.TRUE,
MarshalledNode.of("true").asBoolean());
+ assertEquals(Boolean.FALSE,
MarshalledNode.of("false").asBoolean());
+ }
+
+ public static class B06_Bean {
+ public String name;
+ public int age;
+ }
+
+ @Test void b06_asBean() {
+ var n = MarshalledNode.of(JsonMap.of("name", "John", "age",
45));
+ var b = n.as(B06_Bean.class);
+ assertNotNull(b);
+ assertEquals("John", b.name);
+ assertEquals(45, b.age);
+ }
+
+ @Test void b07_wrongTypeReturnsNull() {
+ // Scalar accessors on container/null nodes return null.
+ assertNull(MarshalledNode.of(JsonMap.of("a", 1)).asString());
+ assertNull(MarshalledNode.of(JsonList.of(1)).asInt());
+ assertNull(MarshalledNode.of(null).asString());
+ assertNull(MarshalledNode.of(null).asInt());
+ // Non-convertible scalar returns null rather than throwing.
+ assertNull(MarshalledNode.of("notanumber").asInt());
+ }
+
+ @Test void b08_value() {
+ var raw = JsonMap.of("a", 1);
+ assertSame(raw, MarshalledNode.of(raw).value());
+ assertEquals("x", MarshalledNode.of("x").value());
+ assertNull(MarshalledNode.of(null).value());
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // c - Navigation
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void c01_getByName() {
+ var n = MarshalledNode.of(JsonMap.of("a", "A", "b",
JsonMap.of("c", "C")));
+ assertEquals("A", n.get("a").asString());
+ assertTrue(n.get("b").isObject());
+ assertEquals("C", n.get("b").get("c").asString());
+ }
+
+ @Test void c02_getByNameMissing() {
+ var n = MarshalledNode.of(JsonMap.of("a", "A"));
+ assertNull(n.get("missing"));
+ }
+
+ @Test void c03_getByNameWrongType() {
+ assertNull(MarshalledNode.of(JsonList.of(1, 2)).get("a"));
+ assertNull(MarshalledNode.of("scalar").get("a"));
+ }
+
+ @Test void c04_getByIndex() {
+ var n = MarshalledNode.of(JsonList.of("x", "y", "z"));
+ assertEquals("x", n.get(0).asString());
+ assertEquals("z", n.get(2).asString());
+ }
+
+ @Test void c05_getByIndexOutOfRange() {
+ var n = MarshalledNode.of(JsonList.of("x"));
+ assertNull(n.get(5));
+ assertNull(n.get(-1));
+ }
+
+ @Test void c06_getByIndexWrongType() {
+ assertNull(MarshalledNode.of(JsonMap.of("a", 1)).get(0));
+ assertNull(MarshalledNode.of("scalar").get(0));
+ }
+
+ @Test void c07_size() {
+ assertEquals(2, MarshalledNode.of(JsonMap.of("a", 1, "b",
2)).size());
+ assertEquals(3, MarshalledNode.of(JsonList.of(1, 2, 3)).size());
+ assertEquals(0, MarshalledNode.of("scalar").size());
+ assertEquals(0, MarshalledNode.of(null).size());
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // d - Fluent build
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void d01_fluentBuild() {
+ var n = MarshalledNode.objectNode()
+ .put("a", 1)
+ .put("b",
MarshalledNode.arrayNode().add("x").add("y").value());
+
+ // Underlying structure.
+ var m = (JsonMap)n.value();
+ assertEquals(1, m.getInt("a"));
+ assertInstanceOf(JsonList.class, m.get("b"));
+ assertEquals("x", m.getList("b").getString(0));
+ assertEquals("y", m.getList("b").getString(1));
+
+ // Serialized JSON output.
+ assertEquals("{\"a\":1,\"b\":[\"x\",\"y\"]}", Json.of(m));
+ }
+
+ @Test void d02_putRequiresObjectNode() {
+ assertThrows(IllegalStateException.class, () ->
MarshalledNode.arrayNode().put("a", 1));
+ assertThrows(IllegalStateException.class, () ->
MarshalledNode.of("scalar").put("a", 1));
+ assertThrows(IllegalStateException.class, () ->
MarshalledNode.of(null).put("a", 1));
+ }
+
+ @Test void d03_addRequiresArrayNode() {
+ assertThrows(IllegalStateException.class, () ->
MarshalledNode.objectNode().add("x"));
+ assertThrows(IllegalStateException.class, () ->
MarshalledNode.of("scalar").add("x"));
+ assertThrows(IllegalStateException.class, () ->
MarshalledNode.of(null).add("x"));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // e - Live view
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_putMutatesBackingMap() {
+ var map = JsonMap.of("a", 1);
+ MarshalledNode.of(map).put("b", 2);
+ assertEquals(2, map.getInt("b"));
+ assertEquals("{\"a\":1,\"b\":2}", Json.of(map));
+ }
+
+ @Test void e02_addMutatesBackingList() {
+ var list = JsonList.of("x");
+ MarshalledNode.of(list).add("y");
+ assertEquals(2, list.size());
+ assertEquals("y", list.getString(1));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // f - Copy independence
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void f01_copyMutateCopyLeavesOriginal() {
+ var original = JsonMap.of("a", 1, "nested", JsonList.of(10,
20));
+ var node = MarshalledNode.of(original);
+ var copy = node.copy();
+
+ ((JsonMap)copy.value()).put("a", 99);
+ copy.get("nested").add(30);
+
+ // Original is unchanged.
+ assertEquals(1, original.getInt("a"));
+ assertEquals(2, original.getList("nested").size());
+ }
+
+ @Test void f02_copyMutateOriginalLeavesCopy() {
+ var original = JsonMap.of("a", 1, "nested", JsonList.of(10,
20));
+ var copy = MarshalledNode.of(original).copy();
+
+ original.put("a", 99);
+ original.getList("nested").add(30);
+
+ // Copy is unchanged.
+ assertEquals(1, copy.get("a").asInt());
+ assertEquals(2, copy.get("nested").size());
+ }
+
+ @Test void f03_copyScalarAndNull() {
+ assertEquals("x", MarshalledNode.of("x").copy().asString());
+ assertTrue(MarshalledNode.of(null).copy().isNull());
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // g - JSON-Pointer (RFC 6901)
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void g01_at() {
+ var n = MarshalledNode.of(JsonMap.of("a", JsonMap.of("b",
JsonList.of("x", "y"))));
+ assertEquals("x", n.at("/a/b/0").asString());
+ assertTrue(n.at("").isObject());
+ }
+
+ @Test void g02_atMissReturnsNull() {
+ var n = MarshalledNode.of(JsonMap.of("a", 1));
+ assertNull(n.at("/missing"));
+ assertNull(n.at("/a/b"));
+ }
+
+ @Test void g03_atPresentNullReturnsNullNode() {
+ var m = new JsonMap();
+ m.put("a", null);
+ var r = MarshalledNode.of(m).at("/a");
+ assertNotNull(r);
+ assertTrue(r.isNull());
+ }
+
+ @Test void g04_find() {
+ var n = MarshalledNode.of(JsonMap.of("a", 1));
+ assertTrue(n.find("/a").isPresent());
+ assertEquals(Integer.valueOf(1), n.find("/a").get().asInt());
+ assertFalse(n.find("/missing").isPresent());
+ }
+
+ @Test void g05_findPresentNull() {
+ var m = new JsonMap();
+ m.put("a", null);
+ var opt = MarshalledNode.of(m).find("/a");
+ assertTrue(opt.isPresent());
+ assertTrue(opt.get().isNull());
+ }
+
+ @Test void g06_set() {
+ var m = new JsonMap();
+ var n = MarshalledNode.of(m);
+ assertSame(n, n.set("/a/b", 1));
+ assertEquals("{\"a\":{\"b\":1}}", Json.of(m));
+ }
+
+ @Test void g07_setAppend() {
+ var m = JsonMap.of("a", JsonList.of(1, 2));
+ MarshalledNode.of(m).set("/a/-", 3);
+ assertEquals("{\"a\":[1,2,3]}", Json.of(m));
+ }
+
+ @Test void g08_setRootThrows() {
+ assertThrows(IllegalArgumentException.class, () ->
MarshalledNode.objectNode().set("", 1));
+ }
+
+ @Test void g09_remove() {
+ var m = JsonMap.of("a", 1, "b", 2);
+ assertEquals(Integer.valueOf(1),
MarshalledNode.of(m).remove("/a"));
+ assertEquals("{\"b\":2}", Json.of(m));
+ assertNull(MarshalledNode.of(m).remove("/missing"));
+ }
+
+ @Test void g10_marshalledMapAt() {
+ var m = JsonMap.of("a", JsonMap.of("b", 1));
+ assertEquals(Integer.valueOf(1), m.at("/a/b").asInt());
+ }
+
+ @Test void g11_marshalledListAt() {
+ var l = JsonList.of("x", "y");
+ assertEquals("y", l.at("/1").asString());
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/objecttools/ObjectRest_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/objecttools/PathTraversal_Test.java
old mode 100755
new mode 100644
similarity index 84%
rename from
juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/objecttools/ObjectRest_Test.java
rename to
juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/objecttools/PathTraversal_Test.java
index 0b5c9953df..7507893dbe
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/objecttools/ObjectRest_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/objecttools/PathTraversal_Test.java
@@ -25,7 +25,6 @@ import java.util.function.*;
import org.apache.juneau.*;
import org.apache.juneau.commons.bean.*;
-import org.apache.juneau.commons.reflect.*;
import org.apache.juneau.marshall.*;
import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.marshall.json5.*;
@@ -37,14 +36,14 @@ import org.junit.jupiter.api.*;
"serial", // serialVersionUID not required for test classes.
"java:S5961" // Test comprehensiveness requires more than 25
assertions.
})
-class ObjectRest_Test extends TestBase {
+class PathTraversal_Test extends TestBase {
//====================================================================================================
// testBasic
//====================================================================================================
@Test void a01_basic() {
- var model = ObjectRest.create(new Json5Map()); // An empty
model.
+ var model = PathTraversal.create(new Json5Map()); // An empty
model.
// Do a PUT
model.put("A", new Json5Map());
@@ -70,7 +69,7 @@ class ObjectRest_Test extends TestBase {
// testBeans
//====================================================================================================
@Test void b01_beans() throws Exception {
- var model = ObjectRest.create(new Json5Map());
+ var model = PathTraversal.create(new Json5Map());
// Java beans.
var p = new Person("some name", 123,
@@ -114,7 +113,7 @@ class ObjectRest_Test extends TestBase {
assertEquals(expectedValue, s);
// Try adding an address
- model = ObjectRest.create(p);
+ model = PathTraversal.create(p);
model.post("addresses", new Address("street C", "city C",
"state C", 12345, true));
s = ((Address)model.get("addresses/2")).toString();
expectedValue = "Address(street=street C,city=city
C,state=state C,zip=12345,isCurrent=true)";
@@ -168,7 +167,7 @@ class ObjectRest_Test extends TestBase {
// Make sure we can get non-existent branches without throwing
any exceptions.
// get() method should just return null.
- model = ObjectRest.create(new Json5Map());
+ model = PathTraversal.create(new Json5Map());
var o = model.get("xxx");
assertEquals("null", (""+o));
@@ -189,7 +188,7 @@ class ObjectRest_Test extends TestBase {
assertEquals("{x:2}", s);
// Make sure doing a POST against "" or "/" adds to the root
object.
- model = ObjectRest.create(new Json5List());
+ model = PathTraversal.create(new Json5List());
model.post("", Integer.valueOf(1));
model.post("/", Integer.valueOf(2));
s = model.get("").toString();
@@ -200,7 +199,7 @@ class ObjectRest_Test extends TestBase {
// testAddressBook
//====================================================================================================
@Test void b02_addressBook() {
- var model = ObjectRest.create(new AddressBook());
+ var model = PathTraversal.create(new AddressBook());
// Try adding a person to the address book.
var billClinton = new Person("Bill Clinton", 65,
@@ -270,10 +269,10 @@ class ObjectRest_Test extends TestBase {
}
//====================================================================================================
- // PojoRest(Object,ReaderParser)
+ // PathTraversal(Object,ReaderParser)
//====================================================================================================
@Test void c01_constructors() {
- var model = ObjectRest.create(new AddressBook(),
Json5Parser.DEFAULT);
+ var model = PathTraversal.create(new AddressBook(),
Json5Parser.DEFAULT);
// Try adding a person to the address book.
var billClinton = new Person("Bill Clinton", 65,
@@ -290,17 +289,17 @@ class ObjectRest_Test extends TestBase {
// setRootLocked()
//====================================================================================================
@Test void d01_rootLocked() {
- var model = ObjectRest.create(new
AddressBook()).setRootLocked();
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
overwrite root object", ()->model.put("", new AddressBook()));
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
overwrite root object", ()->model.put(null, new AddressBook()));
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
overwrite root object", ()->model.put("/", new AddressBook()));
+ var model = PathTraversal.create(new
AddressBook()).setRootLocked();
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
overwrite root object", ()->model.put("", new AddressBook()));
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
overwrite root object", ()->model.put(null, new AddressBook()));
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
overwrite root object", ()->model.put("/", new AddressBook()));
}
//====================================================================================================
// getRootObject()
//====================================================================================================
@Test void e01_getRootObject() {
- var model = ObjectRest.create(new AddressBook());
+ var model = PathTraversal.create(new AddressBook());
assertTrue(model.getRootObject() instanceof AddressBook);
model.put("", "foobar");
assertTrue(model.getRootObject() instanceof String);
@@ -329,13 +328,13 @@ class ObjectRest_Test extends TestBase {
// getJsonList(String url, JsonList defVal)
//====================================================================================================
@Test void f01_getMethods() throws Exception {
- var model = ObjectRest.create(new A());
+ var model = PathTraversal.create(new A());
var l = Json5List.ofString("[{a:'b'}]");
var m = Json5Map.ofString("{a:'b'}");
var jm = new JsonMap(m);
var jl = new JsonList(l);
- assertMapped(model, ObjectRest::get,
+ assertMapped(model, PathTraversal::get,
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"<null>,0,0,false,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
@@ -343,7 +342,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"foo,0,0,false,foo,foo,foo,foo,foo,foo,foo");
- assertMapped(model, ObjectRest::getString,
+ assertMapped(model, PathTraversal::getString,
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"<null>,0,0,false,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
@@ -351,7 +350,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"foo,0,0,false,foo,foo,foo,foo,foo,foo,foo");
- assertMapped(model, ObjectRest::getInt,
+ assertMapped(model, PathTraversal::getInt,
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"<null>,0,0,0,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
@@ -359,7 +358,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"1,0,0,0,1,1,1,1,1,1,1");
- assertMapped(model, ObjectRest::getLong,
+ assertMapped(model, PathTraversal::getLong,
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"<null>,0,0,0,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
@@ -367,7 +366,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"1,0,0,0,1,1,1,1,1,1,1");
- assertMapped(model, ObjectRest::getBoolean,
+ assertMapped(model, PathTraversal::getBoolean,
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"<null>,false,false,false,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
@@ -375,7 +374,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"true,false,false,false,true,true,true,true,true,true,true");
- BiFunction<ObjectRest,String,Object> f1 = (r,p) -> {
+ BiFunction<PathTraversal,String,Object> f1 = (r,p) -> {
try {
return r.getMap(p);
} catch (Exception e) {
@@ -399,7 +398,7 @@ class ObjectRest_Test extends TestBase {
assertEquals("{a:'b'}", model.getMap("f7", m).toString());
assertEquals("{a:'b'}", model.getMap("f8", m).toString());
- BiFunction<ObjectRest,String,Object> f2 = (r,p) -> {
+ BiFunction<PathTraversal,String,Object> f2 = (r,p) -> {
try {
return r.getMap(p, m);
} catch (Exception e) {
@@ -411,7 +410,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f2a,f3,f3a,f4,f4a,f5,f6,f7,f8",
"{a=b},InvalidDataConversionException,{a=b},InvalidDataConversionException,{a=b},InvalidDataConversionException,{a=b},{a=b},{a=b},{a=b},{a=b}");
- BiFunction<ObjectRest,String,Object> f3 = (r,p) -> {
+ BiFunction<PathTraversal,String,Object> f3 = (r,p) -> {
try {
return r.getJsonMap(p);
} catch (Exception e) {
@@ -423,7 +422,7 @@ class ObjectRest_Test extends TestBase {
"f1,f2,f3,f4,f2a,f3a,f4a,f5,f6,f7,f8",
"<null>,InvalidDataConversionException,InvalidDataConversionException,InvalidDataConversionException,<null>,<null>,<null>,<null>,<null>,<null>,<null>");
- BiFunction<ObjectRest,String,Object> f4 = (r,p) -> {
+ BiFunction<PathTraversal,String,Object> f4 = (r,p) -> {
try {
return r.getJsonMap(p, jm);
} catch (Exception e) {
@@ -735,35 +734,11 @@ class ObjectRest_Test extends TestBase {
}
}
-
//====================================================================================================
- // invokeMethod(String url, String method, String args)
-
//====================================================================================================
- @Test void f02_invokeMethod() throws Exception {
-
- var model = ObjectRest.create(new AddressBook().init());
- assertEquals("Person(name=Bill Clinton,age=65)",
model.invokeMethod("0", "toString", ""));
-
- model = ObjectRest.create(new AddressBook().init(),
Json5Parser.DEFAULT);
- assertEquals("Person(name=Bill Clinton,age=65)",
model.invokeMethod("0", "toString", ""));
- assertEquals("NY", model.invokeMethod("0/addresses/0/state",
"toString", ""));
- assertNull(model.invokeMethod("1", "toString", ""));
- }
-
-
//====================================================================================================
- // getPublicMethods(String url)
-
//====================================================================================================
- @Test void f03_getPublicMethods() {
- var model = ObjectRest.create(new AddressBook().init());
-
assertTrue(Json5Serializer.DEFAULT.toString(model.getPublicMethods("0")).contains("'toString'"));
-
assertTrue(Json5Serializer.DEFAULT.toString(model.getPublicMethods("0/addresses/0/state")).contains("'toString'"));
- assertNull(model.getPublicMethods("1"));
- }
-
//====================================================================================================
// getClassMeta(String url)
//====================================================================================================
@Test void f04_getClassMeta() {
- var model = ObjectRest.create(new AddressBook().init());
+ var model = PathTraversal.create(new AddressBook().init());
assertEquals("Person",
model.getClassMeta("0").inner().getSimpleName());
assertEquals("String",
model.getClassMeta("0/addresses/0/state").inner().getSimpleName());
assertNull(model.getClassMeta("1"));
@@ -774,7 +749,7 @@ class ObjectRest_Test extends TestBase {
// g - get(url, Type, Type[]) and getWithDefault(url, def, Type,
Type[]) - parameterized typed getters
//====================================================================================================
@Test void g01_getParameterized_listOfStrings() {
- var model =
ObjectRest.create(Json5Map.ofString("{a:['x','y','z']}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{a:['x','y','z']}"));
List<String> v = model.get("a", LinkedList.class, String.class);
assertEquals(3, v.size());
assertEquals("x", v.get(0));
@@ -782,20 +757,20 @@ class ObjectRest_Test extends TestBase {
}
@Test void g02_getParameterized_mapOfStrings() {
- var model =
ObjectRest.create(Json5Map.ofString("{a:{x:'1',y:'2'}}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{a:{x:'1',y:'2'}}"));
Map<String,Integer> v = model.get("a", TreeMap.class,
String.class, Integer.class);
assertEquals(2, v.size());
assertEquals(Integer.valueOf(1), v.get("x"));
}
@Test void g03_getParameterized_nullReturnsNull() {
- var model = ObjectRest.create(new Json5Map());
+ var model = PathTraversal.create(new Json5Map());
List<String> v = model.get("missing", LinkedList.class,
String.class);
assertNull(v);
}
@Test void g04_getWithDefaultParameterized_returnsDefault() {
- var model = ObjectRest.create(new Json5Map());
+ var model = PathTraversal.create(new Json5Map());
var def = new LinkedList<String>();
def.add("default");
List<String> v = model.getWithDefault("missing", def,
LinkedList.class, String.class);
@@ -803,7 +778,7 @@ class ObjectRest_Test extends TestBase {
}
@Test void g05_getWithDefaultParameterized_returnsConverted() {
- var model =
ObjectRest.create(Json5Map.ofString("{a:['x','y']}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{a:['x','y']}"));
var def = new LinkedList<String>();
List<String> v = model.getWithDefault("a", def,
LinkedList.class, String.class);
assertEquals(2, v.size());
@@ -815,21 +790,21 @@ class ObjectRest_Test extends TestBase {
//====================================================================================================
@Test void h01_put_deepPathInMap() {
// MarshalledMap.put now returns the prior value from the
underlying LinkedHashMap,
- // matching the previous-value contract documented on
ObjectRest.put.
+ // matching the previous-value contract documented on
PathTraversal.put.
var inner = new Json5Map();
inner.put("c", "oldVal");
var middle = new Json5Map();
middle.put("b", inner);
var outer = new Json5Map();
outer.put("a", middle);
- var model = ObjectRest.create(outer);
+ var model = PathTraversal.create(outer);
var prev = model.put("a/b/c", "newVal");
assertEquals("oldVal", prev);
assertEquals("newVal", model.get("a/b/c"));
}
@Test void h02_put_intoList() {
- var model =
ObjectRest.create(Json5Map.ofString("{l:['a','b','c']}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{l:['a','b','c']}"));
var prev = model.put("l/1", "B");
assertEquals("b", prev);
assertEquals("B", model.get("l/1"));
@@ -839,20 +814,20 @@ class ObjectRest_Test extends TestBase {
// parentUrl resolves through a null intermediate -> getNode
returns null -> 404
var m = new HashMap<String,Object>();
m.put("a", null);
- var model = ObjectRest.create(m);
- assertThrowsWithMessage(ObjectRestException.class, "Node at URL
'a/b' not found.", ()->model.put("a/b/c", "v"));
+ var model = PathTraversal.create(m);
+ assertThrowsWithMessage(PathTraversalException.class, "Node at
URL 'a/b' not found.", ()->model.put("a/b/c", "v"));
}
@Test void h03_put_parentMissing_404() {
// When a parent URL resolves to a missing key in a Map, the
resolved JsonNode has o==null and cm
// defaults to Object; service() detects this and returns 404.
- var model = ObjectRest.create(new Json5Map());
- assertThrowsWithMessage(ObjectRestException.class, "Node at URL
'missing' not found.", ()->model.put("missing/x", "v"));
+ var model = PathTraversal.create(new Json5Map());
+ assertThrowsWithMessage(PathTraversalException.class, "Node at
URL 'missing' not found.", ()->model.put("missing/x", "v"));
}
@Test void h04_put_intoBean() {
// BeanMap.put may not return prior value depending on impl; we
verify the put took effect.
- var model = ObjectRest.create(new AddressBook().init());
+ var model = PathTraversal.create(new AddressBook().init());
model.put("0/name", "Hillary Clinton");
assertEquals("Hillary Clinton", model.get("0/name"));
}
@@ -862,7 +837,7 @@ class ObjectRest_Test extends TestBase {
var arr = new String[]{"a","b","c"};
var map = new Json5Map();
map.put("arr", arr);
- var model = ObjectRest.create(map);
+ var model = PathTraversal.create(map);
var result = model.put("arr/1", "B");
assertEquals("arr/1", result);
assertEquals("B", ((String[])map.get("arr"))[1]);
@@ -870,7 +845,7 @@ class ObjectRest_Test extends TestBase {
@Test void h06_put_intoArray_parentBean() {
var p = new Person("a", 1, new Address("s","c","st",1,true),
new Address("s2","c2","st2",2,false));
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
var newAddr = new Address("xs","xc","xst",99,true);
var result = model.put("addresses/0", newAddr);
assertEquals("addresses/0", result);
@@ -880,7 +855,7 @@ class ObjectRest_Test extends TestBase {
@Test void h07_put_intoArray_listIndexOutOfBounds_expands() {
// setArrayEntry expansion path (a.length <= index)
var p = new Person("a", 1, new Address("s","c","st",1,true));
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
var newAddr = new Address("xs","xc","xst",99,true);
var result = model.put("addresses/3", newAddr);
assertEquals("addresses/3", result);
@@ -890,15 +865,15 @@ class ObjectRest_Test extends TestBase {
@Test void h08_put_nonIntegerArrayIndexThrows() {
var p = new Person("a", 1, new Address("s","c","st",1,true));
- var model = ObjectRest.create(p);
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
address an item in an array with a non-integer key",
()->model.put("addresses/foo", new Address()));
+ var model = PathTraversal.create(p);
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
address an item in an array with a non-integer key",
()->model.put("addresses/foo", new Address()));
}
@Test void h09_put_intoLeafTypeThrows() {
// PUT into a node whose parent is a bean property of type
"Object" -- when leaf is a String, can't traverse further
- var model =
ObjectRest.create(Json5Map.ofString("{name:'foo'}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{name:'foo'}"));
// "name" resolves to a String; trying to PUT name/x means the
parent ("name") is leaf of unsupported type
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
perform PUT on", ()->model.put("name/x", "v"));
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
perform PUT on", ()->model.put("name/x", "v"));
}
@Test void h11_put_setNotListThrows() {
@@ -907,14 +882,14 @@ class ObjectRest_Test extends TestBase {
s.add("a");
var m = new HashMap<String,Object>();
m.put("s", s);
- var model = ObjectRest.create(m);
- assertThrowsWithMessage(ObjectRestException.class, "Cannot PUT
to indexed position in non-List collection 's/0'", ()->model.put("s/0", "x"));
+ var model = PathTraversal.create(m);
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
PUT to indexed position in non-List collection 's/0'", ()->model.put("s/0",
"x"));
}
@Test void h10_put_mapWithBeanValueType_convertsMap() {
// Cover convert() bean+Map path: put a HashMap into a typed
map slot whose value type is Address
var p = new Person("a", 1, new Address("s","c","st",1,true));
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
var asMap = new HashMap<String,Object>();
asMap.put("street","S2"); asMap.put("city","C2");
asMap.put("state","ST2"); asMap.put("zip",2); asMap.put("isCurrent",false);
model.put("addresses/0", asMap);
@@ -925,19 +900,19 @@ class ObjectRest_Test extends TestBase {
// i - POST operations
//====================================================================================================
@Test void i01_post_toCollection_returnsIndexedUrl() {
- var model = ObjectRest.create(Json5Map.ofString("{l:['a']}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{l:['a']}"));
var ret = model.post("l", "b");
assertEquals("l/1", ret);
}
@Test void i02_post_toRoot_listAddsAndReturnsUrl() {
- var model = ObjectRest.create(new Json5List());
+ var model = PathTraversal.create(new Json5List());
var ret = model.post("", "x");
assertEquals("/0", ret);
}
@Test void i03_post_toRoot_array() {
- var model = ObjectRest.create(new String[]{"a","b"});
+ var model = PathTraversal.create(new String[]{"a","b"});
var ret = model.post("", "c");
assertEquals("/2", ret);
var arr = (String[])model.getRootObject();
@@ -946,20 +921,20 @@ class ObjectRest_Test extends TestBase {
}
@Test void i04_post_toRoot_invalidTypeThrows() {
- var model = ObjectRest.create(new Json5Map());
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
perform POST on", ()->model.post("", "v"));
+ var model = PathTraversal.create(new Json5Map());
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
perform POST on", ()->model.post("", "v"));
}
@Test void i05_post_missingNode_404() {
// Same scenario as h03 — a missing key resolves to an
Object-typed node; service() detects this and returns 404.
- var model = ObjectRest.create(new Json5Map());
- assertThrowsWithMessage(ObjectRestException.class, "Node at URL
'missing' not found.", ()->model.post("missing", "v"));
+ var model = PathTraversal.create(new Json5Map());
+ assertThrowsWithMessage(PathTraversalException.class, "Node at
URL 'missing' not found.", ()->model.post("missing", "v"));
}
@Test void i06_post_toArray_parentMap() {
var map = new Json5Map();
map.put("arr", new String[]{"a","b"});
- var model = ObjectRest.create(map);
+ var model = PathTraversal.create(map);
var ret = model.post("arr", "c");
assertEquals("arr/2", ret);
assertEquals(3, ((String[])map.get("arr")).length);
@@ -967,7 +942,7 @@ class ObjectRest_Test extends TestBase {
@Test void i07_post_toArray_parentBean() {
var p = new Person("a", 1, new Address("s","c","st",1,true));
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
var newAddr = new Address("xs","xc","xst",99,true);
var ret = model.post("addresses", newAddr);
assertEquals("addresses/1", ret);
@@ -978,7 +953,7 @@ class ObjectRest_Test extends TestBase {
// POST to root Set: c.add returns true, but `c instanceof
List` false -> service returns null.
var s = new LinkedHashSet<String>();
s.add("a");
- var model = ObjectRest.create(s);
+ var model = PathTraversal.create(s);
var ret = model.post("", "b");
assertNull(ret);
assertTrue(s.contains("b"));
@@ -990,29 +965,29 @@ class ObjectRest_Test extends TestBase {
s.add("a");
var m = new HashMap<String,Object>();
m.put("s", s);
- var model = ObjectRest.create(m);
+ var model = PathTraversal.create(m);
var ret = model.post("s", "b");
assertNull(ret);
assertTrue(s.contains("b"));
}
@Test void i08_post_toLeafTypeThrows() {
- var model =
ObjectRest.create(Json5Map.ofString("{name:'foo'}"));
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
perform POST on", ()->model.post("name", "v"));
+ var model =
PathTraversal.create(Json5Map.ofString("{name:'foo'}"));
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
perform POST on", ()->model.post("name", "v"));
}
//====================================================================================================
// j - DELETE operations
//====================================================================================================
@Test void j01_delete_fromMap() {
- var model =
ObjectRest.create(Json5Map.ofString("{a:'x',b:'y'}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{a:'x',b:'y'}"));
var prev = model.delete("a");
assertEquals("x", prev);
assertNull(model.get("a"));
}
@Test void j02_delete_fromList() {
- var model =
ObjectRest.create(Json5Map.ofString("{l:['a','b','c']}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{l:['a','b','c']}"));
var prev = model.delete("l/1");
assertEquals("b", prev);
var l = (List<?>)model.get("l");
@@ -1024,7 +999,7 @@ class ObjectRest_Test extends TestBase {
@Test void j03_delete_fromArray_parentMap() {
var map = new Json5Map();
map.put("arr", new String[]{"a","b","c"});
- var model = ObjectRest.create(map);
+ var model = PathTraversal.create(map);
var prev = model.delete("arr/1");
assertEquals("b", prev);
assertEquals(2, ((String[])map.get("arr")).length);
@@ -1032,14 +1007,14 @@ class ObjectRest_Test extends TestBase {
@Test void j04_delete_fromArray_parentBean() {
var p = new Person("a", 1, new Address("s","c","st",1,true),
new Address("s2","c2","st2",2,false));
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
var prev = (Address)model.delete("addresses/0");
assertEquals("c", prev.city);
assertEquals(1, p.addresses.length);
}
@Test void j05_delete_fromBean_setsNull() {
- var model = ObjectRest.create(new AddressBook().init());
+ var model = PathTraversal.create(new AddressBook().init());
// DELETE on bean property invokes BeanMap.put(key, null)
model.delete("0/name");
var p = (Person)model.get("0");
@@ -1047,21 +1022,21 @@ class ObjectRest_Test extends TestBase {
}
@Test void j06_delete_root() {
- var model = ObjectRest.create(Json5Map.ofString("{a:'x'}"));
+ var model = PathTraversal.create(Json5Map.ofString("{a:'x'}"));
var prev = model.delete("");
assertEquals("{a:'x'}", prev.toString());
assertNull(model.getRootObject());
}
@Test void j07_delete_root_locked() {
- var model = ObjectRest.create(new Json5Map()).setRootLocked();
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
overwrite root object", ()->model.delete(""));
+ var model = PathTraversal.create(new
Json5Map()).setRootLocked();
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
overwrite root object", ()->model.delete(""));
}
@Test void j08_delete_nonIntegerArrayIndex() {
var p = new Person("a", 1, new Address("s","c","st",1,true));
- var model = ObjectRest.create(p);
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
address an item in an array with a non-integer key",
()->model.delete("addresses/foo"));
+ var model = PathTraversal.create(p);
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
address an item in an array with a non-integer key",
()->model.delete("addresses/foo"));
}
@Test void j09a_delete_setNotListThrows() {
@@ -1070,14 +1045,14 @@ class ObjectRest_Test extends TestBase {
s.add("a"); s.add("b");
var m = new HashMap<String,Object>();
m.put("s", s);
- var model = ObjectRest.create(m);
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
DELETE indexed position in non-List collection 's/0'", ()->model.delete("s/0"));
+ var model = PathTraversal.create(m);
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
DELETE indexed position in non-List collection 's/0'", ()->model.delete("s/0"));
}
@Test void j09_delete_unsupportedLeafTypeThrows() {
// DELETE on a leaf node whose parent type isn't
map/list/array/bean
- var model =
ObjectRest.create(Json5Map.ofString("{name:'foo'}"));
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
perform PUT on", ()->model.delete("name/x"));
+ var model =
PathTraversal.create(Json5Map.ofString("{name:'foo'}"));
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
perform PUT on", ()->model.delete("name/x"));
}
//====================================================================================================
@@ -1085,38 +1060,38 @@ class ObjectRest_Test extends TestBase {
//====================================================================================================
@Test void k01_get_nullValueInMap() {
// get path that traverses through a null value in a map
- var model = ObjectRest.create(Json5Map.ofString("{a:null}"));
+ var model = PathTraversal.create(Json5Map.ofString("{a:null}"));
assertNull(model.get("a/b"));
}
@Test void k02_get_listIndexOutOfBounds_returnsNull() {
- var model =
ObjectRest.create(Json5Map.ofString("{l:['a','b']}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{l:['a','b']}"));
assertNull(model.get("l/5"));
}
@Test void k03_get_arrayIndexOutOfBounds_returnsNull() {
- var model = ObjectRest.create(new String[]{"a","b"});
+ var model = PathTraversal.create(new String[]{"a","b"});
assertNull(model.get("5"));
}
@Test void k04_get_arrayElement() {
- var model = ObjectRest.create(new String[]{"a","b","c"});
+ var model = PathTraversal.create(new String[]{"a","b","c"});
assertEquals("b", model.get("1"));
}
@Test void k05_get_listNonIntegerKeyThrows() {
- var model =
ObjectRest.create(Json5Map.ofString("{l:['a','b']}"));
- assertThrowsWithMessage(ObjectRestException.class, "Cannot
address an item in an array with a non-integer key", ()->model.get("l/foo"));
+ var model =
PathTraversal.create(Json5Map.ofString("{l:['a','b']}"));
+ assertThrowsWithMessage(PathTraversalException.class, "Cannot
address an item in an array with a non-integer key", ()->model.get("l/foo"));
}
@Test void k06_get_beanUnknownPropertyThrows() {
- var model = ObjectRest.create(new AddressBook().init());
- assertThrowsWithMessage(ObjectRestException.class, "Unknown
property", ()->model.get("0/notARealField"));
+ var model = PathTraversal.create(new AddressBook().init());
+ assertThrowsWithMessage(PathTraversalException.class, "Unknown
property", ()->model.get("0/notARealField"));
}
@Test void k07_get_blankAndSlashReturnRoot() {
var m = new Json5Map();
- var model = ObjectRest.create(m);
+ var model = PathTraversal.create(m);
assertSame(m, model.get(""));
assertSame(m, model.get("/"));
assertSame(m, model.get(null));
@@ -1124,7 +1099,7 @@ class ObjectRest_Test extends TestBase {
@Test void k08_get_deepPathThroughMixedTypes() {
var p = new Person("a", 1, new
Address("s","city1","st",1,true), new Address("s","city2","st",2,false));
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
assertEquals("city2", model.get("addresses/1/city"));
}
@@ -1135,7 +1110,7 @@ class ObjectRest_Test extends TestBase {
s.add("x"); s.add("y");
var m = new HashMap<String,Object>();
m.put("s", s);
- var model = ObjectRest.create(m);
+ var model = PathTraversal.create(m);
// Indexing into a Set traverses through the (cm.isCollection()
&& !instanceof List) branch
// and returns a JsonNode with o=null cm=Object since ct2 stays
null. The result is null.
assertNull(model.get("s/0"));
@@ -1145,25 +1120,16 @@ class ObjectRest_Test extends TestBase {
// Bean property whose value is null - traversal returns null
var p = new Person();
p.name = null;
- var model = ObjectRest.create(p);
+ var model = PathTraversal.create(p);
assertNull(model.get("name/sub"));
}
-
//====================================================================================================
- // l - invokeMethod exception path
-
//====================================================================================================
- @Test void l01_invokeMethod_invalidMethodThrows() {
- var model = ObjectRest.create(new AddressBook().init());
- // Invoking a method that doesn't exist should yield
ExecutableException
- assertThrows(ExecutableException.class,
()->model.invokeMethod("0", "noSuchMethodOnPerson", ""));
- }
-
//====================================================================================================
// m - JsonNode constructor branch coverage (cm == null path, cm ==
object path)
//====================================================================================================
@Test void m01_jsonNode_nullValueGetsObjectMeta() {
- // Construct ObjectRest over null - JsonNode constructor takes
the o==null branch in cm assignment
- var model = ObjectRest.create(null);
+ // Construct PathTraversal over null - JsonNode constructor
takes the o==null branch in cm assignment
+ var model = PathTraversal.create(null);
assertNull(model.getRootObject());
// PUT "" replaces root even when starting from null
model.put("", "x");
@@ -1171,7 +1137,7 @@ class ObjectRest_Test extends TestBase {
}
@Test void m02_create_withParserUsesParserSession() {
- var model = ObjectRest.create(new Json5Map(),
Json5Parser.DEFAULT);
+ var model = PathTraversal.create(new Json5Map(),
Json5Parser.DEFAULT);
model.put("a", "v");
assertEquals("v", model.get("a"));
}
@@ -1180,18 +1146,18 @@ class ObjectRest_Test extends TestBase {
// n - URI/path syntax: leading slashes, empty segments
//====================================================================================================
@Test void n01_path_leadingSlashIgnored() {
- var model = ObjectRest.create(Json5Map.ofString("{a:'v'}"));
+ var model = PathTraversal.create(Json5Map.ofString("{a:'v'}"));
assertEquals("v", model.get("a"));
assertEquals("v", model.get("/a"));
}
@Test void n02_path_listIndexNavigation() {
- var model =
ObjectRest.create(Json5Map.ofString("{l:[{x:1},{x:2},{x:3}]}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{l:[{x:1},{x:2},{x:3}]}"));
assertEquals(2, model.getInt("l/1/x"));
}
@Test void n03_post_emptyRootIsOk() {
- var model = ObjectRest.create(new Json5List());
+ var model = PathTraversal.create(new Json5List());
model.post(null, "x");
model.post("/", "y");
var l = (List<?>)model.getRootObject();
@@ -1204,7 +1170,7 @@ class ObjectRest_Test extends TestBase {
// o - Patch-like behavior: mix of put/delete on the same model
//====================================================================================================
@Test void o01_patch_likeWorkflow() {
- var model =
ObjectRest.create(Json5Map.ofString("{user:{name:'Alice',roles:['admin','editor']}}"));
+ var model =
PathTraversal.create(Json5Map.ofString("{user:{name:'Alice',roles:['admin','editor']}}"));
model.put("user/name", "Bob");
model.delete("user/roles/0");
model.post("user/roles", "viewer");
@@ -1214,4 +1180,4 @@ class ObjectRest_Test extends TestBase {
assertEquals("editor", roles.get(0));
assertEquals("viewer", roles.get(1));
}
-}
\ No newline at end of file
+}
diff --git
a/juneau-integration-tests/src/test/java/org/apache/juneau/BasicRuntimeException_Test.java
b/juneau-integration-tests/src/test/java/org/apache/juneau/BasicRuntimeException_Test.java
index efa435f7f0..ff17beef1f 100644
---
a/juneau-integration-tests/src/test/java/org/apache/juneau/BasicRuntimeException_Test.java
+++
b/juneau-integration-tests/src/test/java/org/apache/juneau/BasicRuntimeException_Test.java
@@ -92,8 +92,8 @@ class BasicRuntimeException_Test extends TestBase {
assertEquals("Message arg1", x.getMessage());
}
- @Test void a08_ObjectRestException_fluentSetters() {
- var x = new ObjectRestException(404, "Original message");
+ @Test void a08_PathTraversalException_fluentSetters() {
+ var x = new PathTraversalException(404, "Original message");
// Test setMessage returns same instance for fluent chaining
assertSame(x, x.setMessage("New message"));
diff --git
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Response_Body_Test.java
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Response_Body_Test.java
index 2d88cc29a1..475bb3bf9a 100644
---
a/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Response_Body_Test.java
+++
b/juneau-integration-tests/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Response_Body_Test.java
@@ -36,6 +36,7 @@ import org.apache.http.entity.*;
import org.apache.http.message.*;
import org.apache.juneau.*;
import org.apache.juneau.marshall.*;
+import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.marshall.json5.*;
import org.apache.juneau.marshall.parser.*;
import org.apache.juneau.marshall.xml.*;
@@ -247,11 +248,11 @@ class RestClient_Response_Body_Test extends TestBase {
var x18 =
testClient().entity(stringEntity("12345")).get().run().getContent().asAbbreviatedString(4);
assertEquals("1...", x18);
- var x20 =
testClient().entity(stringEntity("{f:1}")).get().run().getContent().asObjectRest(ABean.class);
- assertString("1", x20.get("f"));
+ var x20 =
testClient().entity(stringEntity("{f:1}")).get().run().getContent().as(JsonMap.class).getAt("f",
String.class);
+ assertString("1", x20);
- var x22 =
testClient().entity(stringEntity("{f:1}")).get().run().getContent().asObjectRest();
- assertString("1", x22.get("f"));
+ var x22 =
testClient().entity(stringEntity("{f:1}")).get().run().getContent().as(JsonMap.class).getAt("f",
String.class);
+ assertString("1", x22);
var x24 =
testClient().entity(stringEntity("foo=123")).get().run().getContent().asMatcher(Pattern.compile("foo=(.*)"));
assertTrue(x24.matches());
diff --git
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
index b55caa28b2..43a79740ef 100644
---
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
+++
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/ResponseContent.java
@@ -39,7 +39,6 @@ import org.apache.juneau.marshall.*;
import org.apache.juneau.marshall.collections.*;
import org.apache.juneau.marshall.httppart.*;
import org.apache.juneau.marshall.oapi.*;
-import org.apache.juneau.marshall.objecttools.*;
import org.apache.juneau.marshall.parser.*;
import org.apache.juneau.marshall.parser.ParseException;
import org.apache.juneau.marshall.stream.*;
@@ -52,6 +51,11 @@ import org.apache.juneau.rest.client.classic.assertion.*;
* An extension of an HttpClient {@link HttpEntity} that provides various
support for converting the body to POJOs and
* other convenience methods.
*
+ * <p>
+ * To quickly retrieve a single value from inside a larger JSON document,
parse the body into a {@link JsonMap} and use
+ * its path-addressing methods — e.g.
<c>getContent().as(JsonMap.<jk>class</jk>).getAt(<jv>path</jv>,
<jv>type</jv>)</c>
+ * or the RFC 6901 form
<c>getContent().as(JsonMap.<jk>class</jk>).at(<jv>pointer</jv>)</c>.
+ *
* <h5 class='section'>See Also:</h5><ul>
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/JuneauRestClientBasics">juneau-rest-client
Basics</a>
* </ul>
@@ -761,41 +765,6 @@ public class ResponseContent implements HttpEntity {
return asMatcher(Pattern.compile(regex, flags));
}
- /**
- * Converts the output from the connection into an {@link JsonMap} and
then wraps that in a {@link ObjectRest}.
- *
- * <p>
- * Useful if you want to quickly retrieve a single value from inside of
a larger JSON document.
- *
- * @return The parsed output wrapped in a {@link ObjectRest}.
- * @throws RestCallException
- * <ul>
- * <li>If the input contains a syntax error or is
malformed, or is not valid for the specified type.
- * <li>If a connection error occurred.
- * </ul>
- */
- public ObjectRest asObjectRest() throws RestCallException {
- return asObjectRest(JsonMap.class);
- }
-
- /**
- * Parses the output from the body into the specified type and then
wraps that in a {@link ObjectRest}.
- *
- * <p>
- * Useful if you want to quickly retrieve a single value from inside of
a larger JSON document.
- *
- * @param innerType The class type of the POJO being wrapped.
- * @return The parsed output wrapped in a {@link ObjectRest}.
- * @throws RestCallException
- * <ul>
- * <li>If the input contains a syntax error or is
malformed, or is not valid for the specified type.
- * <li>If a connection error occurred.
- * </ul>
- */
- public ObjectRest asObjectRest(Class<?> innerType) throws
RestCallException {
- return new ObjectRest(as(innerType));
- }
-
/**
* Returns the HTTP response message body as a reader based on the
charset on the <code>Content-Type</code> response header.
*
diff --git
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
index 710c3bc5ec..b0204d7a05 100644
---
a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
+++
b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
@@ -101,7 +101,6 @@ import org.apache.juneau.marshall.marshaller.*;
import org.apache.juneau.marshall.marshaller.Uon;
import org.apache.juneau.marshall.msgpack.*;
import org.apache.juneau.marshall.oapi.*;
-import org.apache.juneau.marshall.objecttools.*;
import org.apache.juneau.marshall.parquet.*;
import org.apache.juneau.marshall.parser.*;
import org.apache.juneau.marshall.parser.ParseException;
@@ -701,8 +700,6 @@ import org.apache.juneau.rest.client.classic.remote.*;
* <li class='jm'><c>{@link ResponseContent#asString() asString()}
<jk>returns</jk> String</c>
* <li class='jm'><c>{@link ResponseContent#asStringFuture()
asStringFuture()} <jk>returns</jk> Future<String></c>
* <li class='jm'><c>{@link
ResponseContent#asAbbreviatedString(int) asAbbreviatedString(int)}
<jk>returns</jk> String</c>
- * <li class='jm'><c>{@link ResponseContent#asObjectRest(Class)
asObjectRest(Class<?>)} <jk>returns</jk> {@link ObjectRest}</c>
- * <li class='jm'><c>{@link ResponseContent#asObjectRest()
asObjectRest()} <jk>returns</jk> {@link ObjectRest}</c>
* <li class='jm'><c>{@link ResponseContent#asMatcher(Pattern)
asMatcher(Pattern)} <jk>returns</jk> {@link Matcher}</c>
* <li class='jm'><c>{@link ResponseContent#asMatcher(String)
asMatcher(String)} <jk>returns</jk> {@link Matcher}</c>
* </ul>
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/converter/Traversable.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/converter/Traversable.java
index d9a899b80e..bc15390ce4 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/converter/Traversable.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/converter/Traversable.java
@@ -25,7 +25,7 @@ import org.apache.juneau.marshall.parser.*;
import org.apache.juneau.rest.server.*;
/**
- * Converter for enabling of {@link ObjectRest} support on response objects
returned by a <c>@RestOp</c>-annotated method.
+ * Converter for enabling path-addressing support on response objects returned
by a <c>@RestOp</c>-annotated method.
*
* <p>
* When enabled, objects in a POJO tree returned by the REST method can be
addressed through additional URL path
@@ -40,7 +40,6 @@ import org.apache.juneau.rest.server.*;
* </p>
*
* <h5 class='section'>See Also:</h5><ul>
- * <li class='jc'>{@link ObjectRest} - Additional information on
addressing elements in a POJO tree using URL notation.
* <li class='ja'>{@link
org.apache.juneau.rest.server.RestOp#converters()} - Registering converters
with REST resources.
* <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/Converters">Converters</a>
* </ul>
@@ -61,9 +60,9 @@ public class Traversable implements RestConverter {
if (nn(swap))
o = swap.swap(bs, o);
ReaderParser rp =
req.getContent().getParserMatch().map(ParserMatch::getParser).filter(ReaderParser.class::isInstance).map(ReaderParser.class::cast).orElse(null);
- var or = ObjectRest.create(o, rp);
+ var or = PathTraversal.create(o, rp);
o = or.get(pathRemainder);
- } catch (ObjectRestException e) {
+ } catch (PathTraversalException e) {
throw new BasicHttpException(e.getStatus(),
null, e);
} catch (Exception t) {
throw new InternalServerError(t);
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/OpenApiVar.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/OpenApiVar.java
index 39e6d3cfa6..f5ec2e1b99 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/OpenApiVar.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/OpenApiVar.java
@@ -59,7 +59,7 @@ public class OpenApiVar extends MultipartResolvingVar {
@Override /* Overridden from Var */
public String resolve(VarResolverSession session, String key) {
RestRequest req =
session.getBean(RestRequest.class).orElseThrow(InternalServerError::new);
- return new ObjectRest(req.getOpenApi()).getString(key);
+ return new PathTraversal(req.getOpenApi()).getString(key);
}
@Override /* Overridden from Var */
diff --git
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/SwaggerVar.java
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/SwaggerVar.java
index 5a2b6452bc..14b76eca31 100644
---
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/SwaggerVar.java
+++
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/server/vars/SwaggerVar.java
@@ -67,7 +67,7 @@ public class SwaggerVar extends MultipartResolvingVar {
@Override /* Overridden from Var */
public String resolve(VarResolverSession session, String key) {
RestRequest req =
session.getBean(RestRequest.class).orElseThrow(InternalServerError::new);
- return new ObjectRest(req.getSwagger()).getString(key);
+ return new PathTraversal(req.getSwagger()).getString(key);
}
@Override /* Overridden from Var */