This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch v4
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/v4 by this push:
     new 0677eae3d1a CAUSEWAY-3897: bit of housekeeping (RO)
0677eae3d1a is described below

commit 0677eae3d1ac59afdba16de737a92b6b99bc52f1
Author: Andi Huber <[email protected]>
AuthorDate: Tue Jul 8 07:29:00 2025 +0200

    CAUSEWAY-3897: bit of housekeeping (RO)
---
 .../org/apache/causeway/commons/io/UrlUtils.java   |   4 +-
 .../restfulobjects/applib/JsonRepresentation.java  | 249 +++++-----------
 .../restfulobjects/applib/LinkRepresentation.java  |  12 +-
 .../restfulobjects/applib/RestfulMediaType.java    |   6 +-
 .../restfulobjects/applib/RestfulResponse.java     | 331 +--------------------
 .../restfulobjects/applib/util/JsonMapper.java     | 109 +++----
 .../restfulobjects/applib/util/JsonNodeUtils.java  |  86 ------
 .../viewer/restfulobjects/applib/util/Links.java   |  36 ---
 .../restfulobjects/applib/util/MediaTypes.java     |  74 -----
 .../viewer/restfulobjects/applib/util/Parser.java  |   7 +-
 .../viewer/restfulobjects/applib/util/Parsers.java |  94 ++++--
 .../restfulobjects/applib/util/PathNode.java       |  33 +-
 .../applib/util/UrlEncodingUtils.java              |  63 ----
 .../applib/util/PathNodeTest_parse.java            |  16 +-
 .../client/src/main/java/module-info.java          |   5 +-
 .../exhandling/ExceptionResponseFactory.java       |   4 +-
 .../JsonValueEncoderServiceDefault.java            |   2 -
 .../context/ResourceContext_getArg_Test.java       |   6 +-
 .../context/ResourceContext_stripQuotes_Test.java  |   2 -
 .../resources/DomainObjectResourceServerside.java  |   9 +-
 .../resources/DomainTypeResourceServerside.java    |   4 +-
 .../resources/MenuBarsResourceServerside.java      |   3 +-
 .../viewer/resources/ResourceAbstract.java         |   9 +
 .../restfulobjects/viewer/header/ParserTest.java   |   8 +-
 24 files changed, 251 insertions(+), 921 deletions(-)

diff --git a/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java 
b/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java
index 94ff598c5ef..5aeb24ae606 100644
--- a/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java
+++ b/commons/src/main/java/org/apache/causeway/commons/io/UrlUtils.java
@@ -42,7 +42,7 @@ public class UrlUtils {
      * @see URLDecoder
      */
     @SneakyThrows
-    public static String urlDecodeUtf8(final @Nullable String input) {
+    public @Nullable String urlDecodeUtf8(final @Nullable String input) {
         return input!=null
              ? URLDecoder.decode(input, StandardCharsets.UTF_8)
              : input;
@@ -54,7 +54,7 @@ public static String urlDecodeUtf8(final @Nullable String 
input) {
      * @see URLEncoder
      */
     @SneakyThrows
-    public static String urlEncodeUtf8(final @Nullable String input) {
+    public @Nullable String urlEncodeUtf8(final @Nullable String input) {
         return input!=null
                 ? URLEncoder.encode(input, StandardCharsets.UTF_8)
                 : input;
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/JsonRepresentation.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/JsonRepresentation.java
index 06a6d9b8e4c..9c83ed679bb 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/JsonRepresentation.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/JsonRepresentation.java
@@ -18,11 +18,13 @@
  */
 package org.apache.causeway.viewer.restfulobjects.applib;
 
+import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.lang.reflect.Constructor;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -48,11 +50,9 @@
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.base._Strings;
-import org.apache.causeway.commons.internal.collections._Maps;
 import org.apache.causeway.commons.io.JsonUtils;
-import org.apache.causeway.viewer.restfulobjects.applib.util.JsonNodeUtils;
+import org.apache.causeway.commons.io.UrlUtils;
 import org.apache.causeway.viewer.restfulobjects.applib.util.PathNode;
-import org.apache.causeway.viewer.restfulobjects.applib.util.UrlEncodingUtils;
 
 /**
  * A wrapper around {@link JsonNode} that provides some additional helper
@@ -81,16 +81,14 @@ public interface HasExtensions {
         public JsonRepresentation getExtensions();
     }
 
-    private static Map<Class<?>, Function<JsonNode, ?>> 
REPRESENTATION_INSTANTIATORS = _Maps.newHashMap();
-    static {
-        REPRESENTATION_INSTANTIATORS.put(String.class, input -> {
+    private final static Map<Class<?>, Function<JsonNode, ?>> 
REPRESENTATION_INSTANTIATORS = Map.of(
+        String.class, input -> {
             if (!input.isTextual()) {
                 throw new IllegalStateException("found node that is not a 
string " + input.toString());
             }
             return input.textValue();
-        });
-        REPRESENTATION_INSTANTIATORS.put(JsonNode.class, input -> input);
-    }
+        },
+        JsonNode.class, input -> input);
 
     private static <T> Function<JsonNode, ?> 
representationInstantiatorFor(final Class<T> representationType) {
         Function<JsonNode, ?> transformer = 
REPRESENTATION_INSTANTIATORS.get(representationType);
@@ -176,9 +174,7 @@ public boolean isValue() {
         return jsonNode.isValueNode();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // getRepresentation
-    // ///////////////////////////////////////////////////////////////////////
+    // -- REPRESENTATION
 
     public JsonRepresentation getRepresentation(final String pathTemplate, 
final Object... args) {
         final String pathStr = String.format(pathTemplate, args);
@@ -192,9 +188,7 @@ public JsonRepresentation getRepresentation(final String 
pathTemplate, final Obj
         return new JsonRepresentation(node);
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isArray, getArray, asArray
-    // ///////////////////////////////////////////////////////////////////////
+    // -- ARRAY
 
     public boolean isArray(final String path) {
         return isArray(getNode(path));
@@ -238,9 +232,7 @@ private JsonRepresentation getArrayEnsured(final String 
path, final JsonNode nod
         return new JsonRepresentation(node).ensureArray();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isMap, getMap, asMap
-    // ///////////////////////////////////////////////////////////////////////
+    // -- MAP
 
     public boolean isMap(final String path) {
         return isMap(getNode(path));
@@ -272,9 +264,7 @@ private JsonRepresentation getMap(final String path, final 
JsonNode node) {
         return new JsonRepresentation(node);
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isNumber
-    // ///////////////////////////////////////////////////////////////////////
+    // -- NUMBER
 
     public boolean isNumber(final String path) {
         return isNumber(getNode(path));
@@ -303,9 +293,7 @@ private Number getNumber(final String path, final JsonNode 
node) {
         return node.numberValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isIntegralNumber, getIntegralNumber, asIntegralNumber
-    // ///////////////////////////////////////////////////////////////////////
+    // -- INTEGRALNUMBER
 
     /**
      * Is a long, an int or a {@link BigInteger}.
@@ -325,81 +313,7 @@ private boolean isIntegralNumber(final JsonNode node) {
         return !representsNull(node) && node.isValueNode() && 
node.isIntegralNumber();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // getDate, asDate
-    // ///////////////////////////////////////////////////////////////////////
-
-//TODO[causeway-viewer-restfulobjects-applib-CAUSEWAY-3892] we have proper 
temporal types since Java 8
-//    public java.util.Date getDate(final String path) {
-//        return getDate(path, getNode(path));
-//    }
-//
-//    public java.util.Date asDate() {
-//        return getDate(null, asJsonNode());
-//    }
-//
-//    private java.util.Date getDate(final String path, final JsonNode node) {
-//        if (representsNull(node)) {
-//            return null;
-//        }
-//        checkValue(path, node, "a date");
-//        if (!node.isTextual()) {
-//            throw new IllegalArgumentException(formatExMsg(path, "is not a 
date"));
-//        }
-//        return JsonTemporalLegacy.fromJsonAsDateOnly(node);
-//    }
-
-    // ///////////////////////////////////////////////////////////////////////
-    // getDateTime, asDateTime
-    // ///////////////////////////////////////////////////////////////////////
-
-//TODO[causeway-viewer-restfulobjects-applib-CAUSEWAY-3892] we have proper 
temporal types since Java 8
-//    public java.util.Date getDateTime(final String path) {
-//        return getDateTime(path, getNode(path));
-//    }
-//
-//    public java.util.Date asDateTime() {
-//        return getDateTime(null, asJsonNode());
-//    }
-//
-//    private java.util.Date getDateTime(final String path, final JsonNode 
node) {
-//        if (representsNull(node)) {
-//            return null;
-//        }
-//        checkValue(path, node, "a date-time");
-//        if (!node.isTextual()) {
-//            throw new IllegalArgumentException(formatExMsg(path, "is not a 
date-time"));
-//        }
-//        return JsonTemporalLegacy.fromJsonAsDateTime(node);
-//    }
-
-    // ///////////////////////////////////////////////////////////////////////
-    // getTime, asTime
-    // ///////////////////////////////////////////////////////////////////////
-
-//TODO[causeway-viewer-restfulobjects-applib-CAUSEWAY-3892] we have proper 
temporal types since Java 8
-//    public java.util.Date getTime(final String path) {
-//        return getTime(path, getNode(path));
-//    }
-//
-//    public java.util.Date asTime() {
-//        return getTime(null, asJsonNode());
-//    }
-//
-//    private java.util.Date getTime(final String path, final JsonNode node) {
-//        if (representsNull(node)) {
-//            return null;
-//        }
-//        checkValue(path, node, "a time");
-//        if (!node.isTextual()) {
-//            throw new IllegalArgumentException(formatExMsg(path, "is not a 
time"));
-//        }
-//        return JsonTemporalLegacy.fromJsonAsTimeOnly(node);
-//    }
-
-    // ///////////////////////////////////////////////////////////////////////
-    // isBoolean, getBoolean, asBoolean
-    // ///////////////////////////////////////////////////////////////////////
+    // -- BOOLEAN
 
     public boolean isBoolean(final String path) {
         return isBoolean(getNode(path));
@@ -438,9 +352,7 @@ private Boolean getBoolean(final String path, final 
JsonNode node) {
         return node.booleanValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isByte, getByte, asByte
-    // ///////////////////////////////////////////////////////////////////////
+    // -- BYTE
 
     /**
      * Use {@link #isIntegralNumber(String)} to test if number (it is not 
possible to check if a byte, however).
@@ -469,9 +381,7 @@ private Byte getByte(final String path, final JsonNode 
node) {
         return node.numberValue().byteValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // getShort, asShort
-    // ///////////////////////////////////////////////////////////////////////
+    // -- SHORT
 
     /**
      * Use {@link #isIntegralNumber(String)} to check if number (it is not 
possible to check if a short, however).
@@ -500,9 +410,7 @@ private Short getShort(final String path, final JsonNode 
node) {
         return node.shortValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // getChar, asChar
-    // ///////////////////////////////////////////////////////////////////////
+    // -- CHAR
 
     /**
      * Use {@link #isString(String)} to check if string (it is not possible to 
check if a character, however).
@@ -534,9 +442,7 @@ private Character getChar(final String path, final JsonNode 
node) {
         return textValue.charAt(0);
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isInt, getInt, asInt
-    // ///////////////////////////////////////////////////////////////////////
+    // -- INT
 
     public boolean isInt(final String path) {
         return isInt(getNode(path));
@@ -576,9 +482,7 @@ private Integer getInt(final String path, final JsonNode 
node) {
         return node.intValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isLong, getLong, asLong
-    // ///////////////////////////////////////////////////////////////////////
+    // -- LONG
 
     public boolean isLong(final String path) {
         return isLong(getNode(path));
@@ -621,9 +525,7 @@ private Long getLong(final String path, final JsonNode 
node) {
         throw new IllegalArgumentException(formatExMsg(path, "is not a long"));
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // getFloat, asFloat
-    // ///////////////////////////////////////////////////////////////////////
+    // -- FLOAT
 
     /**
      * Use {@link #isDecimal(String)} to test if a decimal value
@@ -651,9 +553,7 @@ private Float getFloat(final String path, final JsonNode 
node) {
         return node.floatValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isDecimal, isDouble, getDouble, asDouble
-    // ///////////////////////////////////////////////////////////////////////
+    // -- DECIMAL
 
     public boolean isDecimal(final String path) {
         return isDecimal(getNode(path));
@@ -693,9 +593,7 @@ private Double getDouble(final String path, final JsonNode 
node) {
         return node.doubleValue();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isBigInteger, getBigInteger, asBigInteger
-    // ///////////////////////////////////////////////////////////////////////
+    // -- BIGINTEGER
 
     public boolean isBigInteger(final String path) {
         return isBigInteger(getNode(path));
@@ -792,9 +690,7 @@ private BigInteger getBigInteger(final String path, final 
JsonNode node) {
         throw new IllegalArgumentException(formatExMsg(path, "is not a 
biginteger, is not any other integral number, is not text parseable as a 
biginteger"));
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // isBigDecimal, getBigDecimal, asBigDecimal
-    // ///////////////////////////////////////////////////////////////////////
+    // -- BIGDECIMAL
 
     public boolean isBigDecimal(final String path) {
         return isBigDecimal(getNode(path));
@@ -903,9 +799,7 @@ private BigDecimal getBigDecimal(final String path, final 
JsonNode node) {
         throw new IllegalArgumentException(formatExMsg(path, "is not a 
bigdecimal, is not any other numeric, is not text parseable as a bigdecimal"));
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // getString, isString, asString
-    // ///////////////////////////////////////////////////////////////////////
+    // -- STRING
 
     public boolean isString(final String path) {
         return isString(getNode(path));
@@ -1073,10 +967,6 @@ private JsonRepresentation getNull(final String path, 
final JsonNode node) {
         return new JsonRepresentation(node);
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // mapValueAsLink
-    // ///////////////////////////////////////////////////////////////////////
-
     /**
      * Convert a representation that contains a single node representing a link
      * into a {@link LinkRepresentation}.
@@ -1089,18 +979,13 @@ public LinkRepresentation mapValueAsLink() {
         return getLink(linkPropertyName);
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // asInputStream
-    // ///////////////////////////////////////////////////////////////////////
-
+    /**
+     * Returns the underlying JSON (UTF-8 String) as {@link 
ByteArrayInputStream}.
+     */
     public InputStream asInputStream() {
-        return JsonNodeUtils.asInputStream(jsonNode);
+        return new 
ByteArrayInputStream(jsonNode.toString().getBytes(StandardCharsets.UTF_8));
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // asArrayNode, asObjectNode
-    // ///////////////////////////////////////////////////////////////////////
-
     /**
      * Convert underlying representation into an array.
      */
@@ -1121,10 +1006,6 @@ protected ObjectNode asObjectNode() {
         return (ObjectNode) asJsonNode();
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // asT
-    // ///////////////////////////////////////////////////////////////////////
-
     /**
      * Convenience to simply &quot;downcast&quot;.
      *
@@ -1141,17 +1022,11 @@ public <T extends JsonRepresentation> T as(final 
Class<T> cls) {
         }
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // asUrlEncoded
-    // ///////////////////////////////////////////////////////////////////////
-
     public String asUrlEncoded() {
-        return UrlEncodingUtils.urlEncode(asJsonNode());
+        return UrlUtils.urlEncodeUtf8(asJsonNode().toString());
     }
 
-    // ///////////////////////////////////////////////////////////////////////
-    // mutable (array)
-    // ///////////////////////////////////////////////////////////////////////
+    // -- MUTABLE (ARRAY)
 
     public JsonRepresentation arrayAdd(final Object value) {
         if (!isArray()) {
@@ -1350,7 +1225,7 @@ public JsonRepresentation mapPut(final String key, final 
Object value) {
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.set(path.getTail(), value != null ? new POJONode(value) : 
NullNode.getInstance());
         return this;
     }
@@ -1363,7 +1238,7 @@ public JsonRepresentation mapPutJsonRepresentation(final 
String key, final JsonR
             return this;
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.set(path.getTail(), value.asJsonNode());
         return this;
     }
@@ -1376,7 +1251,7 @@ public JsonRepresentation mapPutString(final String key, 
final String value) {
             return this;
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.put(path.getTail(), value);
         return this;
     }
@@ -1389,7 +1264,7 @@ public JsonRepresentation mapPutJsonNode(final String 
key, final JsonNode value)
             return this;
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.set(path.getTail(), value);
         return this;
     }
@@ -1415,7 +1290,7 @@ public JsonRepresentation mapPutInt(final String key, 
final int value) {
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.put(path.getTail(), value);
         return this;
     }
@@ -1429,7 +1304,7 @@ public JsonRepresentation mapPutLong(final String key, 
final long value) {
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.put(path.getTail(), value);
         return this;
     }
@@ -1443,7 +1318,7 @@ public JsonRepresentation mapPutFloat(final String key, 
final float value) {
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.put(path.getTail(), value);
         return this;
     }
@@ -1457,7 +1332,7 @@ public JsonRepresentation mapPutDouble(final String key, 
final double value) {
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.put(path.getTail(), value);
         return this;
     }
@@ -1471,7 +1346,7 @@ public JsonRepresentation mapPutBoolean(final String key, 
final boolean value) {
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         node.put(path.getTail(), value);
         return this;
     }
@@ -1493,7 +1368,7 @@ public JsonRepresentation mapPutBigInteger(final String 
key, final BigInteger va
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         if (value != null) {
             node.put(path.getTail(), value.toString());
         } else {
@@ -1519,7 +1394,7 @@ public JsonRepresentation mapPutBigDecimal(final String 
key, final BigDecimal va
             throw new IllegalStateException("does not represent map");
         }
         final Path path = Path.parse(key);
-        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), 
path.getHead());
+        final ObjectNode node = walkNodeUpTo(asObjectNode(), path.getHead());
         if (value != null) {
             node.put(path.getTail(), value.toString());
         } else {
@@ -1604,9 +1479,7 @@ public JsonRepresentation setValue(final 
JsonRepresentation value) {
         }
     };
 
-    // ///////////////////////////////////////////////////////////////////////
-    // helpers
-    // ///////////////////////////////////////////////////////////////////////
+    // -- HELPERS
 
     /**
      * A reciprocal of the behaviour of the automatic dereferencing of arrays
@@ -1645,10 +1518,10 @@ private NodeAndFormat getNodeAndFormat(final String 
path) {
         String format = null;
         for (final String key : keys) {
             final PathNode pathNode = PathNode.parse(key);
-            if (!pathNode.getKey().isEmpty()) {
+            if (!pathNode.key().isEmpty()) {
                 // grab format (if present) before moving down the path
                 format = getFormatValueIfAnyFrom(jsonNode);
-                jsonNode = jsonNode.path(pathNode.getKey());
+                jsonNode = jsonNode.path(pathNode.key());
             } else {
                 // pathNode is criteria only; don't change jsonNode
             }
@@ -1752,4 +1625,40 @@ public String toString() {
         return jsonNode.toString();
     }
 
+    // -- UTIL
+
+    /**
+     * Walks the path, ensuring keys exist and are maps, or creating required
+     * maps as it goes.
+     *
+     * <p>For example, if given a list ("a", "b", "c") and starting with an 
empty
+     * map, then will create:
+     *
+     * <pre>
+     * {
+     *   "a": {
+     *     "b: {
+     *       "c": {
+     *       }
+     *     }
+     *   }
+     * }
+     * </pre>
+     */
+    private static ObjectNode walkNodeUpTo(ObjectNode node, final List<String> 
keys) {
+        for (final String key : keys) {
+            JsonNode jsonNode = node.get(key);
+            if (jsonNode == null) {
+                jsonNode = new ObjectNode(JsonNodeFactory.instance);
+                node.set(key, jsonNode);
+            } else {
+                if (!jsonNode.isObject()) {
+                    throw new IllegalArgumentException(String.format("walking 
path: '%s', existing key '%s' is not a map", keys, key));
+                }
+            }
+            node = (ObjectNode) jsonNode;
+        }
+        return node;
+    }
+
 }
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/LinkRepresentation.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/LinkRepresentation.java
index d10106b2cbb..291807a296f 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/LinkRepresentation.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/LinkRepresentation.java
@@ -117,15 +117,9 @@ public int hashCode() {
 
     @Override
     public boolean equals(final Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
         final LinkRepresentation other = (LinkRepresentation) obj;
         if (getHref() == null) {
             if (other.getHref() != null) {
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulMediaType.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulMediaType.java
index 397cf69749b..649b5fbaffa 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulMediaType.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulMediaType.java
@@ -18,12 +18,12 @@
  */
 package org.apache.causeway.viewer.restfulobjects.applib;
 
+import org.springframework.web.bind.annotation.RequestMapping;
+
 /**
  * Media types including the <tt>profile</tt> parameter.
  *
- * <p>
- * Because these values are used in the <tt>@Produces</tt> annotation on the 
jax-rs
- * resources, they must be constants and must be strings.
+ * <p> Because these values are used with {@link RequestMapping} annotations, 
they must be constant strings.
  *
  * @see <a 
href="http://buzzword.org.uk/2009/draft-inkster-profile-parameter-00.html";>buzzword.org.uk</a>
  *
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulResponse.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulResponse.java
index 8a90069d85d..d1ed806719f 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulResponse.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/RestfulResponse.java
@@ -18,27 +18,11 @@
  */
 package org.apache.causeway.viewer.restfulobjects.applib;
 
-import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
 import java.util.Date;
-import java.util.Map;
-
-//import jakarta.ws.rs.core.MultivaluedMap;
-//import jakarta.ws.rs.core.Response;
-//import jakarta.ws.rs.core.Response.Status;
-//import jakarta.ws.rs.core.Response.Status.Family;
-//import jakarta.ws.rs.core.Response.StatusType;
-
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.JsonNode;
 
 import org.springframework.http.CacheControl;
 import org.springframework.http.MediaType;
 
-import org.apache.causeway.commons.internal.collections._Maps;
-import org.apache.causeway.viewer.restfulobjects.applib.util.JsonMapper;
 import org.apache.causeway.viewer.restfulobjects.applib.util.Parser;
 
 /**
@@ -46,329 +30,26 @@
  */
 public class RestfulResponse<T> {
 
-//    public static final class HttpStatusCode {
-//
-//        private static final Map<Status, HttpStatusCode> statii = 
_Maps.newHashMap();
-//        private static final Map<Integer, HttpStatusCode> statusCodes = 
_Maps.newHashMap();
-//
-//        private static class StatusTypeImpl implements StatusType {
-//
-//            private final int statusCode;
-//            private final Family family;
-//            private final String reasonPhrase;
-//
-//            private StatusTypeImpl(final int statusCode, final Family 
family, final String reasonPhrase) {
-//                this.statusCode = statusCode;
-//                this.family = family;
-//                this.reasonPhrase = reasonPhrase;
-//            }
-//
-//            @Override
-//            public int getStatusCode() {
-//                return statusCode;
-//            }
-//
-//            @Override
-//            public Family getFamily() {
-//                return family;
-//            }
-//
-//            @Override
-//            public String getReasonPhrase() {
-//                return reasonPhrase;
-//            }
-//        }
-//
-//        public static HttpStatusCode lookup(final int status) {
-//            return statusCodes.get(status);
-//        }
-//
-//        public static Family lookupFamily(final int statusCode) {
-//            switch (statusCode / 100) {
-//            case 1:
-//                return Family.INFORMATIONAL;
-//            case 2:
-//                return Family.SUCCESSFUL;
-//            case 3:
-//                return Family.REDIRECTION;
-//            case 4:
-//                return Family.CLIENT_ERROR;
-//            case 5:
-//                return Family.SERVER_ERROR;
-//            default:
-//                return Family.OTHER;
-//            }
-//        }
-//
-//        // public static final int SC_CONTINUE = 100;
-//        // public static final int SC_SWITCHING_PROTOCOLS = 101;
-//        // public static final int SC_PROCESSING = 102;
-//
-//        public static final HttpStatusCode OK = new HttpStatusCode(200, 
Status.OK);
-//        public static final HttpStatusCode CREATED = new HttpStatusCode(201, 
Status.CREATED);
-//
-//        // public static final int SC_ACCEPTED = 202;
-//        // public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
-//
-//        public static final HttpStatusCode NO_CONTENT = new 
HttpStatusCode(204, Status.NO_CONTENT);
-//
-//        // public static final int SC_RESET_CONTENT = 205;
-//        // public static final int SC_PARTIAL_CONTENT = 206;
-//        // public static final int SC_MULTI_STATUS = 207;
-//        // public static final int SC_MULTIPLE_CHOICES = 300;
-//        // public static final int SC_MOVED_PERMANENTLY = 301;
-//        // public static final int SC_MOVED_TEMPORARILY = 302;
-//        // public static final int SC_SEE_OTHER = 303;
-//        public static final HttpStatusCode NOT_MODIFIED = new 
HttpStatusCode(304, Status.BAD_REQUEST);
-//
-//        // public static final int SC_NOT_MODIFIED = 304;
-//        // public static final int SC_USE_PROXY = 305;
-//        // public static final int SC_TEMPORARY_REDIRECT = 307;
-//
-//        public static final HttpStatusCode BAD_REQUEST = new 
HttpStatusCode(400, Status.BAD_REQUEST);
-//        public static final HttpStatusCode UNAUTHORIZED = new 
HttpStatusCode(401, Status.UNAUTHORIZED);
-//
-//        // public static final int SC_PAYMENT_REQUIRED = 402;
-//        public static final HttpStatusCode FORBIDDEN = new 
HttpStatusCode(403, Status.FORBIDDEN);
-//
-//        public static final HttpStatusCode NOT_FOUND = new 
HttpStatusCode(404, Status.NOT_FOUND);
-//        public static final HttpStatusCode METHOD_NOT_ALLOWED = new 
HttpStatusCode(405, new StatusTypeImpl(405, Family.CLIENT_ERROR, "Method not 
allowed"));
-//        public static final HttpStatusCode NOT_ACCEPTABLE = new 
HttpStatusCode(406, Status.NOT_ACCEPTABLE);
-//
-//        // public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
-//        // public static final int SC_REQUEST_TIMEOUT = 408;
-//
-//        public static final HttpStatusCode CONFLICT = new 
HttpStatusCode(409, Status.CONFLICT);
-//
-//        // public static final int SC_GONE = 410;
-//        // public static final int SC_LENGTH_REQUIRED = 411;
-//        // public static final int SC_PRECONDITION_FAILED = 412;
-//        // public static final int SC_REQUEST_TOO_LONG = 413;
-//        // public static final int SC_REQUEST_URI_TOO_LONG = 414;
-//        // public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
-//
-//        public static final HttpStatusCode UNSUPPORTED_MEDIA_TYPE = new 
HttpStatusCode(415, Status.UNSUPPORTED_MEDIA_TYPE);
-//
-//        // public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
-//        // public static final int SC_EXPECTATION_FAILED = 417;
-//        // public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419;
-//
-//        public static final HttpStatusCode METHOD_FAILURE = new 
HttpStatusCode(420, new StatusTypeImpl(420, Family.CLIENT_ERROR, "Method 
failure"));
-//
-//        // public static final int SC_UNPROCESSABLE_ENTITY = 422;
-//        public static final HttpStatusCode VALIDATION_FAILED = new 
HttpStatusCode(422, new StatusTypeImpl(422, Family.CLIENT_ERROR, "Validation 
failed"));
-//
-//        // public static final int SC_LOCKED = 423;
-//        // public static final int SC_FAILED_DEPENDENCY = 424;
-//
-//        public static final HttpStatusCode PRECONDITION_HEADER_MISSING = new 
HttpStatusCode(428, new StatusTypeImpl(428, Family.CLIENT_ERROR, "Precondition 
header missing"));
-//
-//        public static final HttpStatusCode INTERNAL_SERVER_ERROR = new 
HttpStatusCode(500, Status.INTERNAL_SERVER_ERROR);
-//        public static final HttpStatusCode NOT_IMPLEMENTED = new 
HttpStatusCode(501, new StatusTypeImpl(501, Family.SERVER_ERROR, "Not 
implemented"));
-//
-//        // public static final int SC_BAD_GATEWAY = 502;
-//        // public static final int SC_SERVICE_UNAVAILABLE = 503;
-//        // public static final int SC_GATEWAY_TIMEOUT = 504;
-//        // public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
-//        // public static final int SC_INSUFFICIENT_STORAGE = 507;
-//
-//        public static final HttpStatusCode statusFor(final int statusCode) {
-//            final HttpStatusCode httpStatusCode = 
statusCodes.get(statusCode);
-//            if (httpStatusCode != null) {
-//                return httpStatusCode;
-//            }
-//            return statusForSynchronized(statusCode);
-//        }
-//
-//        public static final HttpStatusCode statusFor(final Status status) {
-//            return statii.get(status);
-//        }
-//
-//        private static final synchronized HttpStatusCode 
statusForSynchronized(final int statusCode) {
-//            HttpStatusCode httpStatusCode = statusCodes.get(statusCode);
-//            if (httpStatusCode != null) {
-//                return httpStatusCode;
-//            }
-//            httpStatusCode = new HttpStatusCode(statusCode, null);
-//            statusCodes.put(statusCode, httpStatusCode);
-//            return httpStatusCode;
-//        }
-//
-//        private final int statusCode;
-//        private final Family family;
-//        private final StatusType jaxrsStatusType;
-//
-//        private HttpStatusCode(final int statusCode, final StatusType 
status) {
-//            this.statusCode = statusCode;
-//            this.jaxrsStatusType = status;
-//            family = lookupFamily(statusCode);
-//            statusCodes.put(statusCode, this);
-//        }
-//
-//        public int getStatusCode() {
-//            return statusCode;
-//        }
-//
-//        public StatusType getJaxrsStatusType() {
-//            return jaxrsStatusType;
-//        }
-//
-//        public Family getFamily() {
-//            return family;
-//        }
-//
-//        @Override
-//        public int hashCode() {
-//            return statusCode;
-//        }
-//
-//        @Override
-//        public boolean equals(final Object obj) {
-//            if (this == obj) {
-//                return true;
-//            }
-//            if (obj == null) {
-//                return false;
-//            }
-//            if (getClass() != obj.getClass()) {
-//                return false;
-//            }
-//            final HttpStatusCode other = (HttpStatusCode) obj;
-//            if (statusCode != other.statusCode) {
-//                return false;
-//            }
-//            return true;
-//        }
-//
-//        @Override
-//        public String toString() {
-//            return "HttpStatusCode " + statusCode + ", " + family;
-//        }
-//
-//    }
-
-    public static class Header<X> {
+    public record Header<X>(
+        String name,
+        Parser<X> parser) {
 
-        public static final Header<String> WARNING = new 
Header<String>("Warning", warningParser());
+        public static final Header<String> WARNING = new 
Header<String>("Warning", Parser.forWarning());
         public static final Header<Date> LAST_MODIFIED = new 
Header<Date>("Last-Modified", Parser.forDate());
         public static final Header<CacheControl> CACHE_CONTROL = new 
Header<CacheControl>("Cache-Control", Parser.forCacheControl());
         public static final Header<MediaType> CONTENT_TYPE = new 
Header<MediaType>("Content-Type", Parser.forMediaType());
         public static final Header<Integer> CONTENT_LENGTH = new 
Header<Integer>("Content-Length", Parser.forInteger());
-        public static final Header<String> ETAG = new Header<String>("ETag", 
Parser.forETag());
-
-        private final String name;
-        private final Parser<X> parser;
-
-        private Header(final String name, final Parser<X> parser) {
-            this.name = name;
-            this.parser = parser;
-        }
-
-        public String getName() {
-            return name;
-        }
+        //public static final Header<String> ETAG = new Header<String>("ETag", 
Parser.forETag());
 
         public X parse(final String value) {
-            return value != null? parser.valueOf(value): null;
+            return value != null ? parser.valueOf(value): null;
         }
 
         public String render(X message) {
             return parser.asString(message);
         }
 
-        private static Parser<String> warningParser() {
-            return new Parser<String>(){
-                private static final String PREFIX = "199 RestfulObjects ";
-
-                @Override
-                public String valueOf(String str) {
-                    return stripPrefix(str, PREFIX);
-                }
-
-                @Override
-                public String asString(String str) {
-                    return PREFIX + str;
-                }
-                private String stripPrefix(String str, String prefix) {
-                    return str.startsWith(prefix) ? 
str.substring(prefix.length()) : str;
-                }
-            };
-        }
-
-    }
-
-//    private final Response response;
-//    private final HttpStatusCode httpStatusCode;
-    //private final Class<T> returnType;
-    private T entity;
-
-//    @SuppressWarnings({ "rawtypes", "unchecked" })
-//    public static RestfulResponse<JsonRepresentation> of(final Response 
response) {
-//        final MediaType jaxRsMediaType = getHeader(response, 
Header.CONTENT_TYPE);
-//        final RepresentationType representationType = 
RepresentationType.lookup(jaxRsMediaType);
-//        final Class<? extends JsonRepresentation> returnType = 
representationType.getRepresentationClass();
-//        return new RestfulResponse(response, returnType);
-//    }
-//
-//    @SuppressWarnings("unchecked")
-//    public static <T extends JsonRepresentation> RestfulResponse<T> 
ofT(final Response response) {
-//        return (RestfulResponse<T>) of(response);
-//    }
-//
-//    private RestfulResponse(final Response response, final Class<T> 
returnType) {
-//        this.response = response;
-//        this.httpStatusCode = HttpStatusCode.statusFor(response.getStatus());
-//        this.returnType = returnType;
-//    }
-//
-//    public HttpStatusCode getStatus() {
-//        return httpStatusCode;
-//    }
-
-//    public T getEntity() throws JsonParseException, JsonMappingException, 
IOException {
-//        if(entity == null) {
-//            // previously this was good enough, but no longer it seems
-//            //entity = JsonMapper.instance().read(response, returnType);
-//
-//            // instead, we do it manually
-//            final JsonNode jsonNode = JsonMapper.instance().read(response, 
JsonNode.class);
-//            try {
-//                final Constructor<T> constructor = 
returnType.getConstructor(JsonNode.class);
-//                entity = constructor.newInstance(jsonNode);
-//            } catch (NoSuchMethodException | InvocationTargetException | 
IllegalAccessException | InstantiationException e) {
-//                throw new RuntimeException(e);
-//            }
-//        }
-//        return entity;
-//    }
-
-//    public <V> V getHeader(final Header<V> header) {
-//        return getHeader(response, header);
-//    }
-//
-//    private static <V> V getHeader(final Response response, final Header<V> 
header) {
-//        final MultivaluedMap<String, Object> metadata = 
response.getMetadata();
-//        // always returns a String
-//        final String value = (String) metadata.getFirst(header.getName());
-//        return header.parse(value);
-//    }
-
-    /**
-     * Convenience that recasts this response as wrapping some other
-     * representation.
-     *
-     * <p>
-     * This would typically be as the results of a content type being an
-     * error rather than a representation returned on success.
-     */
-    @SuppressWarnings("unchecked")
-    public <Q extends JsonRepresentation> RestfulResponse<Q> wraps(Class<Q> 
cls) {
-        return (RestfulResponse<Q>) this;
     }
 
-//    @Override
-//    public String toString() {
-//        return "RestfulResponse [httpStatusCode=" + httpStatusCode + "]";
-//    }
 
 }
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonMapper.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonMapper.java
index 80245e8c95e..dc32ec04907 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonMapper.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonMapper.java
@@ -32,7 +32,6 @@
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JsonDeserializer;
 import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.JsonSerializer;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
@@ -44,46 +43,57 @@
 /**
  * @since 1.x {@index}
  */
-public final class JsonMapper {
-
-//    public static String getEntityAsStringFrom(final Response response) {
-//
-//        final Object result = response.getEntity();
-//
-//        if(result == null)
-//            return null;
-//
-//        if(result instanceof String) {
-//            return (String) result;
-//        }
-//
-//        // TODO [andi-huber] just a wild guess
-//        return response.readEntity(String.class);
-//
-//        // legacy code ...
-//        // final ClientResponse<?> clientResponse = (ClientResponse<?>) 
response;
-//        // return clientResponse.getEntity(String.class);
-//    }
+public record JsonMapper(
+    ObjectMapper objectMapper,
+    PrettyPrinting prettyPrinting) {
 
     public enum PrettyPrinting {
         ENABLE,
         DISABLE
     }
 
+    /**
+     * Returns a {@link 
org.apache.causeway.viewer.restfulobjects.applib.util.JsonMapper.PrettyPrinting#ENABLE
 pretty-printing enabled} JSON mapper.
+     */
+    public static final JsonMapper instance() {
+        return instance(PrettyPrinting.ENABLE);
+    }
+
+    public static final JsonMapper instance(final PrettyPrinting 
prettyPrinting) {
+        return instanceByConfig.computeIfAbsent(prettyPrinting, 
JsonMapper::new);
+    }
+
+    public JsonRepresentation read(final String json) throws 
JsonParseException, JsonMappingException, IOException {
+        return read(json, JsonRepresentation.class);
+    }
+
+    public <T> T read(final String json, final Class<T> requiredType) throws 
JsonParseException, JsonMappingException, IOException {
+        return objectMapper.readValue(json, requiredType);
+    }
+
+    public String write(final Object object) throws JsonGenerationException, 
JsonMappingException, IOException {
+        return objectMapper.writeValueAsString(object);
+    }
+
+    // -- HELPER
+
+    // non canonical constructor
+    private JsonMapper(final PrettyPrinting prettyPrinting) {
+        this(createObjectMapper(prettyPrinting), prettyPrinting);
+    }
+
     private static final class JsonRepresentationDeserializer extends 
JsonDeserializer<JsonRepresentation> {
         @Override
         public JsonRepresentation deserialize(final JsonParser jp, final 
DeserializationContext ctxt) throws IOException {
-            JsonNode node = jp.getCodec().readTree(jp);
-            return new JsonRepresentation(node);
+            return new JsonRepresentation(jp.getCodec().readTree(jp));
         }
     }
 
     private static final class JsonRepresentationSerializer extends 
JsonSerializer<Object> {
         @Override
-        public void serialize(final Object value, final JsonGenerator jgen, 
final SerializerProvider provider) throws IOException, JsonProcessingException {
-            final JsonRepresentation jsonRepresentation = (JsonRepresentation) 
value;
-            final JsonNode jsonNode = jsonRepresentation.asJsonNode();
-            jgen.writeTree(jsonNode);
+        public void serialize(final Object value, final JsonGenerator jgen, 
final SerializerProvider provider)
+            throws IOException, JsonProcessingException {
+            jgen.writeTree(((JsonRepresentation) value).asJsonNode());
         }
     }
 
@@ -103,49 +113,4 @@ private static ObjectMapper createObjectMapper(final 
PrettyPrinting prettyPrinti
 
     private static Map<PrettyPrinting, JsonMapper> instanceByConfig = new 
ConcurrentHashMap<>();
 
-    /**
-     * Returns a {@link 
org.apache.causeway.viewer.restfulobjects.applib.util.JsonMapper.PrettyPrinting#ENABLE
 pretty-printing enabled} JSON mapper.
-     */
-    public static final JsonMapper instance() {
-        return instance(PrettyPrinting.ENABLE);
-    }
-
-    public static final JsonMapper instance(final PrettyPrinting 
prettyPrinting) {
-        final JsonMapper jsonMapper = instanceByConfig.get(prettyPrinting);
-        if (jsonMapper != null) {
-            return jsonMapper;
-        }
-        // there could be a race-condition here, but it doesn't matter; last 
one wins.
-        final JsonMapper mapper = new JsonMapper(prettyPrinting);
-        instanceByConfig.put(prettyPrinting, mapper);
-
-        return mapper;
-    }
-
-    private final ObjectMapper objectMapper;
-
-    private JsonMapper(final PrettyPrinting prettyPrinting) {
-        objectMapper = createObjectMapper(prettyPrinting);
-    }
-
-    public JsonRepresentation read(final String json) throws 
JsonParseException, JsonMappingException, IOException {
-        return read(json, JsonRepresentation.class);
-    }
-
-    public <T> T read(final String json, final Class<T> requiredType) throws 
JsonParseException, JsonMappingException, IOException {
-        return objectMapper.readValue(json, requiredType);
-    }
-
-//    public <T> T read(final Response response, final Class<T> requiredType) 
throws JsonParseException, JsonMappingException, IOException {
-//        final String entity = getEntityAsStringFrom(response);
-//        if (entity == null) {
-//            return null;
-//        }
-//        return read(entity, requiredType);
-//    }
-
-    public String write(final Object object) throws JsonGenerationException, 
JsonMappingException, IOException {
-        return objectMapper.writeValueAsString(object);
-    }
-
 }
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonNodeUtils.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonNodeUtils.java
deleted file mode 100644
index ef908183f68..00000000000
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/JsonNodeUtils.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- *  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.causeway.viewer.restfulobjects.applib.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
-
-/**
- * @since 1.x {@index}
- */
-public class JsonNodeUtils {
-
-    private JsonNodeUtils() {
-    }
-
-    public static InputStream asInputStream(final JsonNode jsonNode) {
-        final String jsonStr = jsonNode.toString();
-        final byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
-        return new ByteArrayInputStream(bytes);
-    }
-
-    public static InputStream asInputStream(final JsonRepresentation 
jsonRepresentation) {
-        final String jsonStr = jsonRepresentation.toString();
-        final byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
-        return new ByteArrayInputStream(bytes);
-    }
-
-    /**
-     * Walks the path, ensuring keys exist and are maps, or creating required
-     * maps as it goes.
-     *
-     * <p>
-     * For example, if given a list ("a", "b", "c") and starting with an empty
-     * map, then will create:
-     *
-     * <pre>
-     * {
-     *   "a": {
-     *     "b: {
-     *       "c": {
-     *       }
-     *     }
-     *   }
-     * }
-     */
-    public static ObjectNode walkNodeUpTo(ObjectNode node, final List<String> 
keys) {
-        for (final String key : keys) {
-            JsonNode jsonNode = node.get(key);
-            if (jsonNode == null) {
-                jsonNode = new ObjectNode(JsonNodeFactory.instance);
-                node.set(key, jsonNode);
-            } else {
-                if (!jsonNode.isObject()) {
-                    throw new IllegalArgumentException(String.format("walking 
path: '%s', existing key '%s' is not a map", keys, key));
-                }
-            }
-            node = (ObjectNode) jsonNode;
-        }
-        return node;
-    }
-
-}
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Links.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Links.java
deleted file mode 100644
index 0059e92e9d2..00000000000
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Links.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- *  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.causeway.viewer.restfulobjects.applib.util;
-
-import org.apache.causeway.applib.layout.links.Link;
-import org.apache.causeway.viewer.restfulobjects.applib.Rel;
-
-import lombok.experimental.UtilityClass;
-
-@UtilityClass
-public class Links {
-    
-    public Link get(
-            final Rel rel,
-            final String href,
-            final String type) {
-        return new Link(rel.getName(), "GET", href, type);
-    }
-    
-}
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/MediaTypes.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/MediaTypes.java
deleted file mode 100644
index 9395150b876..00000000000
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/MediaTypes.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- *  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.causeway.viewer.restfulobjects.applib.util;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import org.springframework.http.MediaType;
-
-import org.apache.causeway.commons.internal.base._Strings;
-
-import lombok.experimental.UtilityClass;
-
-/**
- * @since 1.x {@index}
- */
-@UtilityClass
-public class MediaTypes {
-
-    /**
-     * Same as {@code MediaType.valueOf(type)}, but with fallback in case 
{@code MediaType.valueOf(type)}
-     * throws an IllegalArgumentException.
-     * <p>
-     * The fallback is to retry with some special characters replaces in 
String {@code type}.
-     *
-     * @param type
-     */
-    public MediaType parse(String type) {
-
-        if(type==null)
-            return MediaType.valueOf(null);
-
-        try {
-
-            return MediaType.valueOf(type);
-
-        } catch (IllegalArgumentException e) {
-
-            List<String> chunks = _Strings.splitThenStream(type, ";")
-                    .collect(Collectors.toList());
-
-            final StringBuilder sb = new StringBuilder();
-            sb.append(chunks.get(0));
-
-            if(chunks.size()>1) {
-                chunks.stream()
-                .skip(1)
-                .map(chunk->chunk.replace(":", "..").replace("/", "."))
-                .forEach(chunk->sb.append(';').append(chunk));
-            }
-
-            return MediaType.valueOf(sb.toString());
-
-        }
-
-    }
-
-}
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parser.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parser.java
index 38b7246ddf1..e7555d634df 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parser.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parser.java
@@ -49,18 +49,15 @@ default T valueOf(final JsonRepresentation 
jsonRepresentation) {
         return valueOf(jsonRepresentation.asString());
     }
 
-    default JsonRepresentation asJsonRepresentation(final T t) {
-        return JsonRepresentation.newMap("dummy", 
asString(t)).getRepresentation("dummy");
-    }
-
     // -- FACTORIES
 
     static Parser<Boolean> forBoolean() { return new Parsers.BooleanParser(); }
     static Parser<Date> forDate() { return new Parsers.DateParser(); }
     static Parser<Integer> forInteger() { return new Parsers.IntegerParser(); }
     static Parser<String> forString() { return new Parsers.StringParser(); }
+    static Parser<String> forWarning() { return new Parsers.WarningParser(); }
     static Parser<MediaType> forMediaType() { return new 
Parsers.MediaTypeParser(); }
-    static Parser<String> forETag() { return new Parsers.ETagParser(); }
+    //static Parser<String> forETag() { return new Parsers.ETagParser(); }
     static Parser<CacheControl> forCacheControl() { return new 
Parsers.CacheControlParser(); }
     static Parser<List<String>> forListOfStrings() { return new 
Parsers.ListOfStringsParser(); }
     static Parser<List<List<String>>> forListOfListOfStrings() { return new 
Parsers.ListOfListOfStringsParser(); }
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parsers.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parsers.java
index 4ada8408fe4..b06a0226885 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parsers.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/Parsers.java
@@ -40,9 +40,9 @@
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
-public class Parsers {
+class Parsers {
 
-    public record BooleanParser() implements Parser<Boolean> {
+    record BooleanParser() implements Parser<Boolean> {
         @Override public Boolean valueOf(final String str) {
             if (str == null) return null;
             return "yes".equalsIgnoreCase(str) || "true".equalsIgnoreCase(str)
@@ -54,7 +54,7 @@ public record BooleanParser() implements Parser<Boolean> {
         }
     }
 
-    public record IntegerParser() implements Parser<Integer> {
+    record IntegerParser() implements Parser<Integer> {
         @Override public Integer valueOf(final String str) {
             if (str == null) return null;
             return Integer.valueOf(str);
@@ -64,12 +64,12 @@ public record IntegerParser() implements Parser<Integer> {
         }
     }
 
-    public record StringParser() implements Parser<String> {
+    record StringParser() implements Parser<String> {
         @Override public String valueOf(final String str) { return str; }
         @Override public String asString(final String t) { return t; }
     }
 
-    public record DateParser() implements Parser<Date> {
+    record DateParser() implements Parser<Date> {
         final static SimpleDateFormat RFC1123_DATE_FORMAT = new 
SimpleDateFormat("EEE, dd MMM yyyyy HH:mm:ss z");
         @Override public Date valueOf(final String str) {
             if (!StringUtils.hasLength(str)) return null;
@@ -86,7 +86,7 @@ public record DateParser() implements Parser<Date> {
         }
     }
 
-    public record MediaTypeParser() implements Parser<MediaType> {
+    record MediaTypeParser() implements Parser<MediaType> {
         @Override public MediaType valueOf(final String str) {
             if (!StringUtils.hasLength(str)) return null;
             return MediaType.valueOf(str);
@@ -96,9 +96,48 @@ public record MediaTypeParser() implements Parser<MediaType> 
{
                     ? t.toString()
                     : null;
         }
+//legacy utility code - perhaps not needed any more
+//        /**
+//         * Same as {@code MediaType.valueOf(type)}, but with fallback in 
case {@code MediaType.valueOf(type)}
+//         * throws an IllegalArgumentException.
+//         * <p>
+//         * The fallback is to retry with some special characters replaces in 
String {@code type}.
+//         *
+//         * @param type
+//         */
+//        private MediaType parse(String type) {
+//
+//            if(type==null) return null;
+//
+//            try {
+//
+//                return MediaType.valueOf(type);
+//
+//            } catch (IllegalArgumentException e) {
+//
+//                List<String> chunks = _Strings.splitThenStream(type, ";")
+//                        .collect(Collectors.toList());
+//
+//                final StringBuilder sb = new StringBuilder();
+//                sb.append(chunks.get(0));
+//
+//                if(chunks.size()>1) {
+//                    chunks.stream()
+//                    .skip(1)
+//                    .map(chunk->chunk.replace(":", "..").replace("/", "."))
+//                    .forEach(chunk->sb.append(';').append(chunk));
+//                }
+//
+//                return MediaType.valueOf(sb.toString());
+//
+//            }
+//
+//        }
     }
 
-    public record CacheControlParser() implements Parser<CacheControl> {
+
+
+    record CacheControlParser() implements Parser<CacheControl> {
         @Override public CacheControl valueOf(String str) {
             //Cache-Control: no-cache, no-store, max-age=3600
             var directives = 
_Strings.splitThenStream(str.toLowerCase(Locale.US), ",")
@@ -115,7 +154,7 @@ public record CacheControlParser() implements 
Parser<CacheControl> {
                             builder = 
CacheControl.maxAge(Integer.parseInt(directive.substring("max-age=".length())), 
TimeUnit.SECONDS);
                         }
                     }
-                };
+                }
             }
             // modifications
             for(String directive : directives) {
@@ -133,7 +172,7 @@ public record CacheControlParser() implements 
Parser<CacheControl> {
                     case "proxy-revalidate" -> builder = 
builder.proxyRevalidate();
                     case "immutable" -> builder = builder.immutable();
                     default -> {}
-                };
+                }
             }
             return builder;
         }
@@ -142,16 +181,16 @@ public record CacheControlParser() implements 
Parser<CacheControl> {
         }
     }
 
-    public record ETagParser() implements Parser<String> {
-        @Override public String valueOf(String str) {
-            return null;
-        }
-        @Override public String asString(String t) {
-            return null;
-        }
-    }
+//    record ETagParser() implements Parser<String> {
+//        @Override public String valueOf(String str) {
+//            return null;
+//        }
+//        @Override public String asString(String t) {
+//            return null;
+//        }
+//    }
 
-    public record ListOfStringsParser() implements Parser<List<String>> {
+    record ListOfStringsParser() implements Parser<List<String>> {
         @Override public List<String> valueOf(final List<String> strings) {
             if (strings == null) return Collections.emptyList();
             if (strings.size() == 1) {
@@ -181,7 +220,7 @@ public record ListOfStringsParser() implements 
Parser<List<String>> {
         }
     }
 
-    public record ListOfListOfStringsParser() implements 
Parser<List<List<String>>> {
+    record ListOfListOfStringsParser() implements Parser<List<List<String>>> {
         @Override public List<List<String>> valueOf(final List<String> str) {
             if (str == null || str.size() == 0) return null;
             final List<List<String>> listOfLists = _Lists.newArrayList();
@@ -207,7 +246,7 @@ public record ListOfListOfStringsParser() implements 
Parser<List<List<String>>>
         }
     }
 
-    public record ArrayOfStringsParser() implements Parser<String[]> {
+    record ArrayOfStringsParser() implements Parser<String[]> {
         @Override public String[] valueOf(final List<String> strings) {
             if (strings == null) return _Constants.emptyStringArray;
             if (strings.size() == 1) {
@@ -236,7 +275,7 @@ public record ArrayOfStringsParser() implements 
Parser<String[]> {
         }
     }
 
-    public record ListOfMediaTypesParser() implements Parser<List<MediaType>> {
+    record ListOfMediaTypesParser() implements Parser<List<MediaType>> {
         @Override public List<MediaType> valueOf(final String str) {
             if (str == null) return Collections.emptyList();
             return _Strings.splitThenStream(str, ",")
@@ -250,4 +289,17 @@ public record ListOfMediaTypesParser() implements 
Parser<List<MediaType>> {
         }
     }
 
+    record WarningParser() implements Parser<String> {
+        private static final String PREFIX = "199 RestfulObjects ";
+        @Override public String valueOf(String str) {
+            return stripPrefix(str, PREFIX);
+        }
+        @Override public String asString(String str) {
+            return PREFIX + str;
+        }
+        private String stripPrefix(String str, String prefix) {
+            return str.startsWith(prefix) ? str.substring(prefix.length()) : 
str;
+        }
+    }
+
 }
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNode.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNode.java
index d493bf71130..305373c8fcf 100644
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNode.java
+++ 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNode.java
@@ -34,13 +34,16 @@
 /**
  * @since 1.x {@index}
  */
-public class PathNode {
+public record PathNode(
+        String key,
+        Map<String, String> criteria
+    ) {
 
     private static final Pattern NODE = 
Pattern.compile("^([^\\[]*)(\\[(.+)\\])?$");
     private static final Pattern WHITESPACE = Pattern.compile("\\s+");
     private static final Pattern LIST_CRITERIA_SYNTAX = 
Pattern.compile("^([^=]+)=(.+)$");
 
-    public static final PathNode NULL = new PathNode("", Collections.<String, 
String> emptyMap());
+    public static final PathNode NULL = new PathNode("", 
Collections.emptyMap());
 
     public static List<String> split(String path) {
         List<String> parts = _Lists.newArrayList();
@@ -103,31 +106,20 @@ public static PathNode parse(final String path) {
         return new PathNode(key, criteria);
     }
 
-    private final String key;
-    private final Map<String, String> criteria;
-
-    private PathNode(final String key, final Map<String, String> criteria) {
+    public PathNode(final String key, final Map<String, String> criteria) {
         this.key = key;
         this.criteria = Collections.unmodifiableMap(criteria);
     }
 
-    public String getKey() {
-        return key;
-    }
-
-    public Map<String, String> getCriteria() {
-        return criteria;
-    }
-
     public boolean hasCriteria() {
-        return !getCriteria().isEmpty();
+        return criteria.isEmpty();
     }
 
     public boolean matches(final JsonRepresentation repr) {
         if (!repr.isMap()) {
             return false;
         }
-        for (final Map.Entry<String, String> criterium : 
getCriteria().entrySet()) {
+        for (final Map.Entry<String, String> criterium : criteria.entrySet()) {
             String requiredValue = criterium.getValue();
             if(requiredValue != null) {
                 // list syntax
@@ -167,12 +159,9 @@ public int hashCode() {
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
         PathNode other = (PathNode) obj;
         if (key == null) {
             if (other.key != null)
diff --git 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java
 
b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java
deleted file mode 100644
index 595c321bacb..00000000000
--- 
a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/util/UrlEncodingUtils.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- *  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.causeway.viewer.restfulobjects.applib.util;
-
-import java.util.Arrays;
-import java.util.List;
-
-import com.fasterxml.jackson.databind.JsonNode;
-
-import org.apache.causeway.commons.internal.collections._Lists;
-import org.apache.causeway.commons.io.UrlUtils;
-import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
-
-import lombok.experimental.UtilityClass;
-
-/**
- * @since 1.x {@index}
- */
-@UtilityClass
-public final class UrlEncodingUtils {
-
-    public String urlDecode(final String string) {
-        return UrlUtils.urlDecodeUtf8(string);
-    }
-
-    public List<String> urlDecode(final List<String> values) {
-        return _Lists.map(values, UrlUtils::urlDecodeUtf8);
-    }
-
-    public String[] urlDecode(final String[] values) {
-        final List<String> asList = Arrays.asList(values);
-        return urlDecode(asList).toArray(new String[] {});
-    }
-
-    public String urlEncode(final JsonNode jsonNode) {
-        return urlEncode(jsonNode.toString());
-    }
-
-    public String urlEncode(final JsonRepresentation jsonRepresentation ) {
-        return urlEncode(jsonRepresentation.toString());
-    }
-
-    public String urlEncode(final String str) {
-        return UrlUtils.urlEncodeUtf8(str);
-    }
-
-}
diff --git 
a/viewers/restfulobjects/applib/src/test/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNodeTest_parse.java
 
b/viewers/restfulobjects/applib/src/test/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNodeTest_parse.java
index 57afcbdfbac..41f530c6f3e 100644
--- 
a/viewers/restfulobjects/applib/src/test/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNodeTest_parse.java
+++ 
b/viewers/restfulobjects/applib/src/test/java/org/apache/causeway/viewer/restfulobjects/applib/util/PathNodeTest_parse.java
@@ -30,15 +30,15 @@ class PathNodeTest_parse {
     @Test
     public void simple() throws Exception {
         final PathNode node = PathNode.parse("foo");
-        assertThat(node.getKey(), is("foo"));
-        assertThat(node.getCriteria().isEmpty(), is(true));
+        assertThat(node.key(), is("foo"));
+        assertThat(node.criteria().isEmpty(), is(true));
     }
 
     @Test
     public void oneCriterium() throws Exception {
         final PathNode node = PathNode.parse("foo[bar=coz]");
-        assertThat(node.getKey(), is("foo"));
-        final Map<String, String> criteria = node.getCriteria();
+        assertThat(node.key(), is("foo"));
+        final Map<String, String> criteria = node.criteria();
         assertThat(criteria.isEmpty(), is(false));
         assertThat(criteria.size(), is(1));
         assertThat(criteria.get("bar"), is("coz"));
@@ -47,8 +47,8 @@ public void oneCriterium() throws Exception {
     @Test
     public void moreThanOneCriterium() throws Exception {
         final PathNode node = PathNode.parse("foo[bar=coz dat=ein]");
-        assertThat(node.getKey(), is("foo"));
-        final Map<String, String> criteria = node.getCriteria();
+        assertThat(node.key(), is("foo"));
+        final Map<String, String> criteria = node.criteria();
         assertThat(criteria.isEmpty(), is(false));
         assertThat(criteria.size(), is(2));
         assertThat(criteria.get("bar"), is("coz"));
@@ -58,8 +58,8 @@ public void moreThanOneCriterium() throws Exception {
     @Test
     public void whiteSpace() throws Exception {
         final PathNode node = PathNode.parse("foo[bar=coz\tdat=ein]");
-        assertThat(node.getKey(), is("foo"));
-        final Map<String, String> criteria = node.getCriteria();
+        assertThat(node.key(), is("foo"));
+        final Map<String, String> criteria = node.criteria();
         assertThat(criteria.isEmpty(), is(false));
         assertThat(criteria.size(), is(2));
         assertThat(criteria.get("bar"), is("coz"));
diff --git a/viewers/restfulobjects/client/src/main/java/module-info.java 
b/viewers/restfulobjects/client/src/main/java/module-info.java
index f376a27cb48..b586a4ec108 100644
--- a/viewers/restfulobjects/client/src/main/java/module-info.java
+++ b/viewers/restfulobjects/client/src/main/java/module-info.java
@@ -17,19 +17,20 @@
  *  under the License.
  */
 module org.apache.causeway.viewer.restfulobjects.client {
-    exports org.apache.causeway.viewer.restfulobjects.client.log;
     exports org.apache.causeway.viewer.restfulobjects.client;
     exports org.apache.causeway.viewer.restfulobjects.client.auth;
     exports org.apache.causeway.viewer.restfulobjects.client.auth.basic;
     exports org.apache.causeway.viewer.restfulobjects.client.auth.oauth2;
     exports org.apache.causeway.viewer.restfulobjects.client.auth.oauth2.azure;
+    exports org.apache.causeway.viewer.restfulobjects.client.log;
+
+    requires static lombok;
 
     requires com.fasterxml.jackson.core;
     requires com.fasterxml.jackson.databind;
     requires jakarta.annotation;
     requires jakarta.ws.rs;
     requires jakarta.xml.bind;
-    requires static lombok;
     requires transitive org.apache.causeway.applib;
     requires transitive org.apache.causeway.commons;
     requires transitive org.apache.causeway.viewer.restfulobjects.applib;
diff --git 
a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/exhandling/ExceptionResponseFactory.java
 
b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/exhandling/ExceptionResponseFactory.java
index bca6eee8734..e53c51598b8 100644
--- 
a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/exhandling/ExceptionResponseFactory.java
+++ 
b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/exhandling/ExceptionResponseFactory.java
@@ -72,7 +72,7 @@ private ResponseEntity<Object> buildResponse(
         var builder = ResponseEntity.status(httpStatus);
 
         if (message != null) {
-            builder = builder.header(RestfulResponse.Header.WARNING.getName(), 
RestfulResponse.Header.WARNING.render(message));
+            builder = builder.header(RestfulResponse.Header.WARNING.name(), 
RestfulResponse.Header.WARNING.render(message));
         }
 
         // hmm; the mediaType doesn't seem to be specified in the RO spec
@@ -137,7 +137,7 @@ private ResponseEntity<Object> buildResponse(
 
         final String message = exceptionPojo.message();
         if (message != null) {
-            builder = builder.header(RestfulResponse.Header.WARNING.getName(), 
RestfulResponse.Header.WARNING.render(message));
+            builder = builder.header(RestfulResponse.Header.WARNING.name(), 
RestfulResponse.Header.WARNING.render(message));
         }
 
         return builder
diff --git 
a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
 
b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
index 3a6af73678a..74829d3aaf1 100644
--- 
a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
+++ 
b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
@@ -61,12 +61,10 @@
 @Slf4j
 public class JsonValueEncoderServiceDefault implements JsonValueEncoderService 
{
 
-    private final SpecificationLoader specificationLoader;
     private final Map<Class<?>, JsonValueConverter> converterByClass;
 
     @Inject
     public JsonValueEncoderServiceDefault(final SpecificationLoader 
specificationLoader) {
-        this.specificationLoader = specificationLoader;
         this.converterByClass = _JsonValueConverters.byClass();
     }
 
diff --git 
a/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_getArg_Test.java
 
b/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_getArg_Test.java
index 115a4f2232c..d4585cf1ef4 100644
--- 
a/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_getArg_Test.java
+++ 
b/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_getArg_Test.java
@@ -41,8 +41,6 @@
 //import 
org.apache.causeway.core.security.authentication.manager.AuthenticationManager;
 import org.apache.causeway.viewer.restfulobjects.applib.JsonRepresentation;
 import 
org.apache.causeway.viewer.restfulobjects.applib.RestfulRequest.RequestParameter;
-import org.apache.causeway.viewer.restfulobjects.applib.util.UrlEncodingUtils;
-import 
org.apache.causeway.viewer.restfulobjects.rendering.context.ResourceContext;
 
 class ResourceContext_getArg_Test {
 
@@ -99,7 +97,7 @@ void setUp() throws Exception {
 
     @Test
     void whenArgExists() throws Exception {
-        final String queryString = 
UrlEncodingUtils.urlEncode(JsonRepresentation.newMap("x-ro-page", 
"123").asJsonNode());
+        final String queryString = JsonRepresentation.newMap("x-ro-page", 
"123").asUrlEncoded();
         resourceContext = ResourceContext.forTesting(queryString, 
mockHttpServletRequest);
         final Integer arg = 
ResourceContext.arg(resourceContext.queryStringAsJsonRepr(), 
RequestParameter.PAGE);
         assertThat(arg, equalTo(123));
@@ -107,7 +105,7 @@ void whenArgExists() throws Exception {
 
     @Test
     void whenArgDoesNotExist() throws Exception {
-        final String queryString = 
UrlEncodingUtils.urlEncode(JsonRepresentation.newMap("xxx", 
"123").asJsonNode());
+        final String queryString = JsonRepresentation.newMap("xxx", 
"123").asUrlEncoded();
         resourceContext = ResourceContext.forTesting(queryString, 
mockHttpServletRequest);
         final Integer arg = 
ResourceContext.arg(resourceContext.queryStringAsJsonRepr(), 
RequestParameter.PAGE);
         assertThat(arg, equalTo(RequestParameter.PAGE.getDefault()));
diff --git 
a/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_stripQuotes_Test.java
 
b/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_stripQuotes_Test.java
index 650b57e9f27..5928e36361a 100644
--- 
a/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_stripQuotes_Test.java
+++ 
b/viewers/restfulobjects/rendering/src/test/java/org/apache/causeway/viewer/restfulobjects/rendering/context/ResourceContext_stripQuotes_Test.java
@@ -24,8 +24,6 @@
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 
-import 
org.apache.causeway.viewer.restfulobjects.rendering.context.ResourceContext;
-
 class ResourceContext_stripQuotes_Test {
 
     @Test
diff --git 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
index 08de00ef8b6..ee85948cf39 100644
--- 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
+++ 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainObjectResourceServerside.java
@@ -46,7 +46,6 @@
 import org.apache.causeway.viewer.restfulobjects.applib.Rel;
 import org.apache.causeway.viewer.restfulobjects.applib.RepresentationType;
 import 
org.apache.causeway.viewer.restfulobjects.applib.domainobjects.DomainObjectResource;
-import org.apache.causeway.viewer.restfulobjects.applib.util.Links;
 import org.apache.causeway.viewer.restfulobjects.rendering.ResponseFactory;
 import 
org.apache.causeway.viewer.restfulobjects.rendering.RestfulObjectsApplicationException;
 import 
org.apache.causeway.viewer.restfulobjects.rendering.context.ResourceContext;
@@ -272,7 +271,7 @@ public static void addLinks(
         grid.visit(new Grid.VisitorAdapter() {
             @Override
             public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
-                Link link = Links.get(
+                Link link = newLink(
                         Rel.ELEMENT,
                         resourceContext.restfulUrlFor(
                                 "objects/" + domainType + "/" + instanceId
@@ -283,7 +282,7 @@ public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
 
             @Override
             public void visit(final ActionLayoutData actionLayoutData) {
-                Link link = Links.get(
+                Link link = newLink(
                         Rel.ACTION,
                         resourceContext.restfulUrlFor(
                                 "objects/" + domainType + "/" + instanceId + 
"/actions/" + actionLayoutData.getId()
@@ -294,7 +293,7 @@ public void visit(final ActionLayoutData actionLayoutData) {
 
             @Override
             public void visit(final PropertyLayoutData propertyLayoutData) {
-                Link link = Links.get(
+                Link link = newLink(
                         Rel.PROPERTY,
                         resourceContext.restfulUrlFor(
                                 "objects/" + domainType + "/" + instanceId + 
"/properties/" + propertyLayoutData.getId()
@@ -305,7 +304,7 @@ public void visit(final PropertyLayoutData 
propertyLayoutData) {
 
             @Override
             public void visit(final CollectionLayoutData collectionLayoutData) 
{
-                Link link = Links.get(
+                Link link = newLink(
                         Rel.COLLECTION,
                         resourceContext.restfulUrlFor(
                                 "objects/" + domainType + "/" + instanceId + 
"/collections/" + collectionLayoutData.getId()
diff --git 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainTypeResourceServerside.java
 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainTypeResourceServerside.java
index 00986011928..018db00de2d 100644
--- 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainTypeResourceServerside.java
+++ 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/DomainTypeResourceServerside.java
@@ -28,6 +28,7 @@
 
 import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.io.UrlUtils;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
@@ -36,7 +37,6 @@
 import org.apache.causeway.viewer.restfulobjects.applib.Rel;
 import org.apache.causeway.viewer.restfulobjects.applib.RepresentationType;
 import 
org.apache.causeway.viewer.restfulobjects.applib.domaintypes.DomainTypeResource;
-import org.apache.causeway.viewer.restfulobjects.applib.util.UrlEncodingUtils;
 import org.apache.causeway.viewer.restfulobjects.rendering.Caching;
 import org.apache.causeway.viewer.restfulobjects.rendering.LinkBuilder;
 import org.apache.causeway.viewer.restfulobjects.rendering.ResponseFactory;
@@ -319,7 +319,7 @@ private static String domainTypeFor(
         }
 
         // formal style; must parse from args that has a link with an href to 
the domain type
-        var requestParams = 
RequestParams.ofQueryString(UrlEncodingUtils.urlDecode(argsAsUrlEncodedQueryString));
+        var requestParams = 
RequestParams.ofQueryString(UrlUtils.urlDecodeUtf8(argsAsUrlEncodedQueryString));
         final String href = linkFromFormalArgs(requestParams, argsParamId, 
onRoException);
         return UrlParserUtils.domainTypeFrom(href);
     }
diff --git 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/MenuBarsResourceServerside.java
 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/MenuBarsResourceServerside.java
index 684728d6dc8..838cb374b99 100644
--- 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/MenuBarsResourceServerside.java
+++ 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/MenuBarsResourceServerside.java
@@ -32,7 +32,6 @@
 import org.apache.causeway.viewer.restfulobjects.applib.Rel;
 import org.apache.causeway.viewer.restfulobjects.applib.RepresentationType;
 import 
org.apache.causeway.viewer.restfulobjects.applib.menubars.MenuBarsResource;
-import org.apache.causeway.viewer.restfulobjects.applib.util.Links;
 import 
org.apache.causeway.viewer.restfulobjects.rendering.RestfulObjectsApplicationException;
 import 
org.apache.causeway.viewer.restfulobjects.rendering.context.ResourceContext;
 import 
org.apache.causeway.viewer.restfulobjects.rendering.service.RepresentationService;
@@ -81,7 +80,7 @@ public static Consumer<ServiceActionLayoutData> 
linksForServiceActionsAddingVisi
                 final String relativeUrl = String.format(
                         "objects/%s/%s/actions/%s",
                         logicalTypeName, SERVICE_IDENTIFIER, 
actionLayoutData.getId());
-                Link link = Links.get(
+                Link link = newLink(
                         Rel.ACTION,
                         resourceContext.restfulUrlFor(relativeUrl),
                         
RepresentationType.OBJECT_ACTION.getJsonMediaType().toString());
diff --git 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/ResourceAbstract.java
 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/ResourceAbstract.java
index 9940d122cec..d50c224043a 100644
--- 
a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/ResourceAbstract.java
+++ 
b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/ResourceAbstract.java
@@ -33,6 +33,7 @@
 import org.springframework.http.HttpStatus;
 
 import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.applib.layout.links.Link;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.functions._Predicates;
@@ -44,6 +45,7 @@
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.viewer.restfulobjects.applib.Rel;
 import org.apache.causeway.viewer.restfulobjects.applib.RepresentationType;
 import org.apache.causeway.viewer.restfulobjects.rendering.ResponseFactory;
 import 
org.apache.causeway.viewer.restfulobjects.rendering.RestfulObjectsApplicationException;
@@ -156,6 +158,13 @@ protected ManagedObject getObjectAdapterElseThrowNotFound(
                                         "Could not determine adapter for 
bookmark: '%s'".formatted(bookmark))));
     }
 
+    protected static Link newLink(
+            final Rel rel,
+            final String href,
+            final String type) {
+        return new Link(rel.getName(), "GET", href, type);
+    }
+
     // -- HELPER
 
     @SneakyThrows
diff --git 
a/viewers/restfulobjects/viewer/src/test/java/org/apache/causeway/viewer/restfulobjects/viewer/header/ParserTest.java
 
b/viewers/restfulobjects/viewer/src/test/java/org/apache/causeway/viewer/restfulobjects/viewer/header/ParserTest.java
index fc976e7bdea..18e2dec8f80 100644
--- 
a/viewers/restfulobjects/viewer/src/test/java/org/apache/causeway/viewer/restfulobjects/viewer/header/ParserTest.java
+++ 
b/viewers/restfulobjects/viewer/src/test/java/org/apache/causeway/viewer/restfulobjects/viewer/header/ParserTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.causeway.viewer.restfulobjects.viewer.header;
 
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import org.junit.jupiter.api.Test;
@@ -30,7 +31,6 @@
 import org.springframework.http.CacheControl;
 
 import org.apache.causeway.viewer.restfulobjects.applib.RestfulMediaType;
-import org.apache.causeway.viewer.restfulobjects.applib.util.MediaTypes;
 import org.apache.causeway.viewer.restfulobjects.applib.util.Parser;
 
 class ParserTest {
@@ -51,12 +51,12 @@ void forCacheControl() {
     void forMediaType() {
         final Parser<org.springframework.http.MediaType> parser = 
Parser.forMediaType();
 
-        for (final org.springframework.http.MediaType v : new 
org.springframework.http.MediaType[] {
+        for (final org.springframework.http.MediaType v : List.of(
                 org.springframework.http.MediaType.APPLICATION_ATOM_XML,
                 org.springframework.http.MediaType.APPLICATION_JSON,
                 org.springframework.http.MediaType.APPLICATION_XHTML_XML,
-                MediaTypes.parse(RestfulMediaType.APPLICATION_JSON_OBJECT)
-        }) {
+                
Parser.forMediaType().valueOf(RestfulMediaType.APPLICATION_JSON_OBJECT)
+        )) {
             final String asString = parser.asString(v);
             final org.springframework.http.MediaType valueOf = 
parser.valueOf(asString);
             assertThat(v, is(equalTo(valueOf)));


Reply via email to