You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2023/02/24 11:01:06 UTC

[causeway] branch 3330-RO.composite.values created (now 482e51140d)

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

ahuber pushed a change to branch 3330-RO.composite.values
in repository https://gitbox.apache.org/repos/asf/causeway.git


      at 482e51140d CAUSEWAY-3330: adds composite value RO test case

This branch includes the following new commits:

     new 482e51140d CAUSEWAY-3330: adds composite value RO test case

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[causeway] 01/01: CAUSEWAY-3330: adds composite value RO test case

Posted by ah...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 3330-RO.composite.values
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 482e51140d2b572e8808ff9288953dc837b00648
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Feb 24 12:01:00 2023 +0100

    CAUSEWAY-3330: adds composite value RO test case
    
    - yet the REST transport is done using a base64 encoded string (in both
    directions)
---
 .../applib/value/semantics/ValueDecomposition.java | 12 ++-
 .../applib/value/CalendarEventSemantics.java       |  2 +-
 .../causeway/testdomain/rest/RestServiceTest.java  | 92 +++++++++-------------
 .../testdomain/jdo/JdoInventoryResource.java       |  8 +-
 .../testdomain/util/rest/RestEndpointService.java  | 24 ++++++
 .../applib/dtos/ScalarValueDtoV2.java              | 15 +++-
 .../client/ActionParameterListBuilder.java         |  6 ++
 .../restfulobjects/client/ResponseDigest.java      |  9 +++
 .../restfulobjects/client/RestfulClient.java       |  2 +-
 ...ntentNegotiationServiceOrgApacheCausewayV2.java | 47 +++++++----
 .../JsonValueEncoderServiceDefault.java            | 12 +++
 .../viewer/resources/JsonParserHelper.java         |  4 +-
 12 files changed, 153 insertions(+), 80 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueDecomposition.java b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueDecomposition.java
index 7f314ff46c..ec957cc094 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueDecomposition.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/value/semantics/ValueDecomposition.java
@@ -23,6 +23,7 @@ import java.io.Serializable;
 import org.apache.causeway.applib.util.schema.CommonDtoUtils;
 import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.functional.Either.HasEither;
+import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.schema.common.v2.TypedTupleDto;
 import org.apache.causeway.schema.common.v2.ValueType;
 import org.apache.causeway.schema.common.v2.ValueWithTypeDto;
@@ -50,7 +51,7 @@ implements
     }
 
     /**
-     * In support of JAXB de-serialization, 
+     * In support of JAXB de-serialization,
      * returns an unspecified type.
      * (Introduced for the CalendarEvent demo to work.)
      * @deprecated not sure why we are hitting this; remove eventually
@@ -76,4 +77,13 @@ implements
             : ofFundamental(CommonDtoUtils.getFundamentalValueFromJson(vType, json));
     }
 
+    // for transport over REST
+    public String stringify() {
+        return _Strings.base64UrlEncodeZlibCompressed(toJson());
+    }
+    // for transport over REST
+    public static ValueDecomposition destringify(final ValueType vType, final String string) {
+        return fromJson(vType, _Strings.base64UrlDecodeZlibCompressed(string));
+    }
+
 }
\ No newline at end of file
diff --git a/extensions/vw/fullcalendar/applib/src/main/java/org/apache/causeway/extensions/fullcalendar/applib/value/CalendarEventSemantics.java b/extensions/vw/fullcalendar/applib/src/main/java/org/apache/causeway/extensions/fullcalendar/applib/value/CalendarEventSemantics.java
index 749e11d392..b6d63fff28 100644
--- a/extensions/vw/fullcalendar/applib/src/main/java/org/apache/causeway/extensions/fullcalendar/applib/value/CalendarEventSemantics.java
+++ b/extensions/vw/fullcalendar/applib/src/main/java/org/apache/causeway/extensions/fullcalendar/applib/value/CalendarEventSemantics.java
@@ -183,7 +183,7 @@ implements
                 ZonedDateTime.of(2022, 05, 13, 17, 30, 15, 0, ZoneOffset.ofHours(3)),
                 "Business",
                 "Weekly Meetup",
-                "Calendar Notes");
+                "Calendar Notes: <a href=\"https://apache.org\">apache.org</a>"); // should be properly serialized to JSON
 
         val b = CalendarEvent.of(
                 ZonedDateTime.of(2022, 06, 14, 18, 31, 16, 0, ZoneOffset.ofHours(4)),
diff --git a/regressiontests/stable-rest/src/test/java/org/apache/causeway/testdomain/rest/RestServiceTest.java b/regressiontests/stable-rest/src/test/java/org/apache/causeway/testdomain/rest/RestServiceTest.java
index 6687032edd..190d456ebc 100644
--- a/regressiontests/stable-rest/src/test/java/org/apache/causeway/testdomain/rest/RestServiceTest.java
+++ b/regressiontests/stable-rest/src/test/java/org/apache/causeway/testdomain/rest/RestServiceTest.java
@@ -22,6 +22,7 @@ import javax.inject.Inject;
 import javax.xml.bind.JAXBException;
 
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.web.server.LocalServerPort;
@@ -33,17 +34,22 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.causeway.core.config.presets.CausewayPresets;
+import org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEventSemantics;
 import org.apache.causeway.testdomain.conf.Configuration_usingJdo;
 import org.apache.causeway.testdomain.jdo.JdoInventoryJaxbVm;
 import org.apache.causeway.testdomain.jdo.JdoTestFixtures;
 import org.apache.causeway.testdomain.jdo.entities.JdoBook;
 import org.apache.causeway.testdomain.util.rest.RestEndpointService;
+import org.apache.causeway.viewer.restfulobjects.client.RestfulClient;
 import org.apache.causeway.viewer.restfulobjects.jaxrsresteasy.CausewayModuleViewerRestfulObjectsJaxrsResteasy;
 
 import lombok.val;
 
 @SpringBootTest(
-        classes = {RestEndpointService.class},
+        classes = {
+                RestEndpointService.class,
+                CalendarEventSemantics.class // register semantics for testing
+                },
         webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 @TestPropertySource(CausewayPresets.UseLog4j2Test)
 @Import({
@@ -55,12 +61,17 @@ class RestServiceTest {
     @LocalServerPort int port; // just for reference (not used)
     @Inject RestEndpointService restService;
 
-    @Test
-    void httpSessionInfo() {
+    private RestfulClient restfulClient;
 
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
+    @BeforeEach
+    void checkPrereq() {
+        assertTrue(restService.getPort()>0);
+        val useRequestDebugLogging = true;
+        this.restfulClient = restService.newClient(useRequestDebugLogging);
+    }
 
+    @Test
+    void httpSessionInfo() {
         val digest = restService.getHttpSessionInfo(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -70,17 +81,10 @@ class RestServiceTest {
 
         // NB: this works only because we excluded wicket viewer from the app.
         assertEquals("no http-session", httpSessionInfo);
-
     }
 
     @Test
     void bookOfTheWeek_viaRestEndpoint() {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val digest = restService.getRecommendedBookOfTheWeek(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -88,17 +92,10 @@ class RestServiceTest {
 
         assertNotNull(bookOfTheWeek);
         assertEquals("Book of the week", bookOfTheWeek.getName());
-
     }
 
     @Test
     void addNewBook_viaRestEndpoint() throws JAXBException {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val newBook = JdoBook.of("REST Book", "A sample REST book for testing.", 77.,
                 "REST Author", "REST ISBN", "REST Publisher");
 
@@ -109,17 +106,10 @@ class RestServiceTest {
 
         assertNotNull(storedBook);
         assertEquals("REST Book", storedBook.getName());
-
     }
 
     @Test
     void multipleBooks_viaRestEndpoint() throws JAXBException {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val digest = restService.getMultipleBooks(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -133,12 +123,6 @@ class RestServiceTest {
 
     @Test
     void bookOfTheWeek_asDto_viaRestEndpoint() {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val digest = restService.getRecommendedBookOfTheWeekAsDto(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -146,17 +130,10 @@ class RestServiceTest {
 
         assertNotNull(bookOfTheWeek);
         assertEquals("Book of the week", bookOfTheWeek.getName());
-
     }
 
     @Test
     void multipleBooks_asDto_viaRestEndpoint() throws JAXBException {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val digest = restService.getMultipleBooksAsDto(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -167,17 +144,10 @@ class RestServiceTest {
         for(val book : multipleBooks) {
             assertEquals("MultipleBooksAsDtoTest", book.getName());
         }
-
     }
 
     @Test
     void inventoryAsJaxbVm_viaRestEndpoint() {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val digest = restService.getInventoryAsJaxbVm(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -185,17 +155,10 @@ class RestServiceTest {
 
         assertNotNull(inventoryAsJaxbVm);
         assertEquals("Bookstore", inventoryAsJaxbVm.getName());
-
     }
 
     @Test
     void listBooks_fromInventoryAsJaxbVm_viaRestEndpoint() {
-
-        assertTrue(restService.getPort()>0);
-
-        val useRequestDebugLogging = false;
-        val restfulClient = restService.newClient(useRequestDebugLogging);
-
         val digest = restService.getBooksFromInventoryAsJaxbVm(restfulClient)
                 .ifFailure(Assertions::fail);
 
@@ -207,8 +170,29 @@ class RestServiceTest {
                 .filter(book->expectedBookTitles.contains(book.getName()));
 
         assertEquals(3, multipleBooks.size());
-
     }
 
+    @Test
+    void calendarEvent_echo_viaRestEndpoint() {
+        val calSemantics = new CalendarEventSemantics();
+        val calSample = calSemantics.getExamples().getElseFail(0);
+        /* calSemantics.decompose(calSample).toJson() ...
+         * {
+         * "elements":[
+         *     {"long":1652452215000,"type":"long","name":"epochMillis"},
+         *     {"string":"Business","type":"string","name":"calendarName"},
+         *     {"string":"Weekly Meetup","type":"string","name":"title"},
+         *     {"string":"Calendar Notes","type":"string","name":"notes"}
+         *     ],
+         * "type":"org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEvent",
+         * "cardinality":4
+         * }
+         */
+        val digest = restService.echoCalendarEvent(restfulClient, calSample)
+                .ifFailure(Assertions::fail);
+
+        val calSampleEchoed = digest.getValue().orElseThrow();
+        assertEquals(calSample, calSampleEchoed);
+    }
 
 }
diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/jdo/JdoInventoryResource.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/jdo/JdoInventoryResource.java
index 157325859d..6dcc50f301 100644
--- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/jdo/JdoInventoryResource.java
+++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/jdo/JdoInventoryResource.java
@@ -38,6 +38,7 @@ import org.apache.causeway.applib.services.factory.FactoryService;
 import org.apache.causeway.applib.services.repository.RepositoryService;
 import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEvent;
 import org.apache.causeway.testdomain.jdo.entities.JdoBook;
 import org.apache.causeway.testdomain.jdo.entities.JdoProduct;
 import org.apache.causeway.testdomain.util.dto.BookDto;
@@ -93,12 +94,17 @@ public class JdoInventoryResource {
         return listBooks();
     }
 
-    @Action //TODO improve the REST client such that the param can be of type Book
+    @Action //XXX improve the REST client such that the param can be of type JdoBook?
     public JdoBook storeBook(final String newBook) throws JAXBException {
         val book = JdoBook.fromDto(BookDto.decode(newBook));
         return repository.persist(book);
     }
 
+    @Action // echos given CalendarEvent (composite value type test)
+    public CalendarEvent echoCalendarEvent(final CalendarEvent calendarEvent) throws JAXBException {
+        return calendarEvent;
+    }
+
     // -- NON - ENTITIES
 
     @Action
diff --git a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/rest/RestEndpointService.java b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/rest/RestEndpointService.java
index f8d2ca8ae0..9dd3853dea 100644
--- a/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/rest/RestEndpointService.java
+++ b/regressiontests/stable/src/main/java/org/apache/causeway/testdomain/util/rest/RestEndpointService.java
@@ -30,10 +30,14 @@ import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.client.SuppressionType;
 import org.apache.causeway.applib.services.iactnlayer.InteractionService;
+import org.apache.causeway.applib.value.semantics.ValueDecomposition;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.core.config.RestEasyConfiguration;
 import org.apache.causeway.core.config.viewer.web.WebAppContextPath;
+import org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEvent;
+import org.apache.causeway.extensions.fullcalendar.applib.value.CalendarEventSemantics;
+import org.apache.causeway.schema.common.v2.ValueType;
 import org.apache.causeway.testdomain.jdo.JdoInventoryJaxbVm;
 import org.apache.causeway.testdomain.jdo.JdoTestFixtures;
 import org.apache.causeway.testdomain.jdo.entities.JdoBook;
@@ -227,6 +231,26 @@ public class RestEndpointService {
         return digest;
     }
 
+    public Try<CalendarEvent> echoCalendarEvent(
+            final RestfulClient client, final CalendarEvent calendarEvent) {
+
+        val calSemantics = new CalendarEventSemantics();
+
+        val request = newInvocationBuilder(client,
+                INVENTORY_RESOURCE + "/actions/echoCalendarEvent/invoke");
+        val args = client.arguments()
+                .addActionParameter("calendarEvent", calSemantics.decompose(calendarEvent))
+                .build();
+
+        val response = request.post(args);
+
+        val digest = client.digest(response, String.class)
+                .mapSuccess(stringifiedVal -> ValueDecomposition.destringify(ValueType.COMPOSITE, stringifiedVal))
+                .mapSuccess(valDecomposition->calSemantics.compose(valDecomposition));
+
+        return digest;
+    }
+
     public Try<String> getHttpSessionInfo(final RestfulClient client) {
 
         val request = newInvocationBuilder(client,
diff --git a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/dtos/ScalarValueDtoV2.java b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/dtos/ScalarValueDtoV2.java
index 5d77a54983..62aef8dccd 100644
--- a/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/dtos/ScalarValueDtoV2.java
+++ b/viewers/restfulobjects/applib/src/main/java/org/apache/causeway/viewer/restfulobjects/applib/dtos/ScalarValueDtoV2.java
@@ -37,12 +37,12 @@ import lombok.NonNull;
 @Data @NoArgsConstructor @AllArgsConstructor(access = AccessLevel.PRIVATE)
 public final class ScalarValueDtoV2 {
 
-   public static ScalarValueDtoV2 forNull(@NonNull Class<?> type) {
-       return new ScalarValueDtoV2(type.getSimpleName(), null);
+   public static ScalarValueDtoV2 forNull(final @NonNull Class<?> type) {
+       return new ScalarValueDtoV2(typeName(type), null);
    }
 
-   public static ScalarValueDtoV2 forValue(@NonNull Object value) {
-       return new ScalarValueDtoV2(value.getClass().getSimpleName(), value);
+   public static ScalarValueDtoV2 forValue(final @NonNull Object value) {
+       return new ScalarValueDtoV2(typeName(value.getClass()), value);
    }
 
    private String type;
@@ -53,4 +53,11 @@ public final class ScalarValueDtoV2 {
        return value == null;
    }
 
+   private static String typeName(final @NonNull Class<?> cls) {
+       return cls.isPrimitive()
+               || cls.getPackageName().startsWith("java.")
+               ? cls.getSimpleName()
+               : cls.getName();
+   }
+
 }
diff --git a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ActionParameterListBuilder.java b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ActionParameterListBuilder.java
index 2cead49a9e..96a23e367c 100644
--- a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ActionParameterListBuilder.java
+++ b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ActionParameterListBuilder.java
@@ -24,6 +24,8 @@ import java.util.stream.Collectors;
 
 import javax.ws.rs.client.Entity;
 
+import org.apache.causeway.applib.value.semantics.ValueDecomposition;
+
 import lombok.Getter;
 
 /**
@@ -86,6 +88,10 @@ public class ActionParameterListBuilder {
         return this;
     }
 
+    public ActionParameterListBuilder addActionParameter(final String parameterName, final ValueDecomposition decomposition) {
+        return addActionParameter(parameterName, decomposition.stringify());
+    }
+
 //XXX would be nice to have, but also requires the RO spec to be updated
 //    public ActionParameterListBuilder addActionParameterDto(String parameterName, Object parameterDto) {
 //        actionParameters.put(parameterName, dto(parameterDto));
diff --git a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigest.java b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigest.java
index 5c4ef3c978..37e4cc5d6d 100644
--- a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigest.java
+++ b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigest.java
@@ -179,6 +179,15 @@ class ResponseDigest<T> {
             failureCause = _Exceptions.unrecoverable(e, "failed to read JAX-RS response content");
         }
 
+        // guard against entity type mismatch
+        failureCause = entities.stream()
+            .filter(entity->!entityType.isAssignableFrom(entity.getClass()))
+            .map(entityOfWrongType->_Exceptions.unrecoverable("type mismatch when digesting REST response, expected: %s, got: %s",
+                    entityType,
+                    entityOfWrongType.getClass()))
+            .findAny()
+            .orElse(null);
+
         return this;
     }
 
diff --git a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClient.java b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClient.java
index a65ba69516..7ce1fcd15f 100644
--- a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClient.java
+++ b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClient.java
@@ -155,7 +155,7 @@ public class RestfulClient implements AutoCloseable {
     public <T> Try<T> digest(final Response response, final Class<T> entityType) {
         final var digest = ResponseDigest.wrap(response, entityType);
         if(digest.isSuccess()) {
-            return Try.success(digest.getEntity().orElse(null));
+            return Try.call(()->digest.getEntity().orElse(null));
         }
         return Try.failure(digest.getFailureCause());
     }
diff --git a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV2.java b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV2.java
index ec7b14abf1..d23b9d0585 100644
--- a/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV2.java
+++ b/viewers/restfulobjects/rendering/src/main/java/org/apache/causeway/viewer/restfulobjects/rendering/service/conneg/ContentNegotiationServiceOrgApacheCausewayV2.java
@@ -20,6 +20,7 @@ package org.apache.causeway.viewer.restfulobjects.rendering.service.conneg;
 
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import javax.inject.Named;
@@ -28,12 +29,15 @@ import javax.ws.rs.core.Response;
 
 import com.fasterxml.jackson.databind.node.POJONode;
 
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.client.RepresentationTypeSimplifiedV2;
 import org.apache.causeway.applib.client.SuppressionType;
+import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.consent.Consent;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
@@ -52,6 +56,7 @@ import org.apache.causeway.viewer.restfulobjects.rendering.Responses;
 import org.apache.causeway.viewer.restfulobjects.rendering.domainobjects.ObjectAndActionInvocation;
 import org.apache.causeway.viewer.restfulobjects.rendering.domainobjects.ObjectPropertyReprRenderer;
 
+import lombok.RequiredArgsConstructor;
 import lombok.val;
 
 /**
@@ -61,7 +66,9 @@ import lombok.val;
 @Named(CausewayModuleViewerRestfulObjectsApplib.NAMESPACE + ".ContentNegotiationServiceOrgApacheCausewayV2")
 @javax.annotation.Priority(PriorityPrecedence.MIDPOINT - 200)
 @Qualifier("OrgApacheCausewayV2")
-public class ContentNegotiationServiceOrgApacheCausewayV2 extends ContentNegotiationServiceAbstract {
+@RequiredArgsConstructor(onConstructor_ = {@Autowired})
+public class ContentNegotiationServiceOrgApacheCausewayV2
+extends ContentNegotiationServiceAbstract {
 
     /**
      * Unlike RO v1.0, use a single content-type of <code>application/json;profile="urn:org.apache.causeway/v2"</code>.
@@ -72,11 +79,6 @@ public class ContentNegotiationServiceOrgApacheCausewayV2 extends ContentNegotia
 
     private final ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0;
 
-    public ContentNegotiationServiceOrgApacheCausewayV2(final ContentNegotiationServiceForRestfulObjectsV1_0 restfulObjectsV1_0) {
-        this.restfulObjectsV1_0 = restfulObjectsV1_0;
-    }
-
-
     /**
      * Domain object is returned as a map with the RO 1.0 representation as a special '$$ro' property
      * within that map.
@@ -263,10 +265,9 @@ public class ContentNegotiationServiceOrgApacheCausewayV2 extends ContentNegotia
 
             objectAndActionInvocation.streamElementAdapters()
             .map(elementAdapter->{
-                val pojo = elementAdapter.getPojo();
-                return pojo==null
-                    ? ScalarValueDtoV2.forNull(elementAdapter.getSpecification().getCorrespondingClass())
-                    : ScalarValueDtoV2.forValue(pojo);
+                val dto = dtoForValue(returnedAdapter)
+                        .orElseGet(()->elementAdapter.getSpecification().getCorrespondingClass());
+                return dto;
             })
             .forEach(rootRepresentation::arrayAdd);
 
@@ -275,16 +276,14 @@ public class ContentNegotiationServiceOrgApacheCausewayV2 extends ContentNegotia
             break;
 
         case SCALAR_VALUE:
-
-            val pojo = returnedAdapter.getPojo();
-            if(pojo==null) {
+            val dto = dtoForValue(returnedAdapter).orElse(null);
+            if(dto==null) {
                 // 404 not found
                 return Responses.ofNotFound();
             }
 
-            val dto = ScalarValueDtoV2.forValue(pojo);
-
-            rootRepresentation = new JsonRepresentation(new POJONode(dto));
+            val jsonNode = new POJONode(dto);
+            rootRepresentation = new JsonRepresentation(jsonNode);
             headerContentType = RepresentationTypeSimplifiedV2.VALUE;
 
             break;
@@ -305,6 +304,22 @@ public class ContentNegotiationServiceOrgApacheCausewayV2 extends ContentNegotia
         return responseBuilder(responseBuilder);
     }
 
+    private Optional<Object> dtoForValue(final @Nullable ManagedObject valueObject) {
+        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(valueObject)
+                || !valueObject.getSpecification().isValue()) {
+            return Optional.empty();
+        }
+        val valSpec = valueObject.getSpecification();
+        val dto = valSpec.isCompositeValue()
+                ? ScalarValueDtoV2.forValue(
+                        ((ValueSemanticsProvider)valSpec.valueFacetElseFail()
+                            .selectDefaultSemantics().orElseThrow()) //TODO honor value semantics context?
+                            .decompose(valueObject.getPojo())
+                            .stringify())
+                : ScalarValueDtoV2.forValue(valueObject.getPojo());
+        return Optional.of(dto);
+    }
+
     /**
      * For easy subclassing to further customize, eg additional headers
      */
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 432bf86479..f3ba178f42 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
@@ -34,7 +34,9 @@ import org.springframework.util.ClassUtils;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.value.semantics.ValueDecomposition;
+import org.apache.causeway.applib.value.semantics.ValueSemanticsProvider;
 import org.apache.causeway.commons.functional.Try;
+import org.apache.causeway.commons.internal.assertions._Assert;
 import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.collections._Maps;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
@@ -91,6 +93,16 @@ public class JsonValueEncoderServiceDefault implements JsonValueEncoderService {
         val valueSerializer =
                 Facets.valueSerializerElseFail(spec, valueClass);
 
+        // handle composite value types (requires a ValueSemanticsProvider for the valueClass to be registered with Spring)
+        if(spec.isCompositeValue()) {
+            _Assert.assertTrue(valueRepr.isString(), ()->"expected to receive a String originating from ValueDecomposition#stringify");
+            val valueFacet = spec.valueFacetElseFail();
+            val valSemantics = (ValueSemanticsProvider<?>)valueFacet.selectDefaultSemantics().orElseThrow();
+            val valDecomposition = ValueDecomposition.destringify(ValueType.COMPOSITE, valueRepr.asString());
+            val pojo = valSemantics.compose(valDecomposition);
+            return ManagedObject.value(spec, pojo);
+        }
+
         final JsonValueConverter jsonValueConverter = converterByClass
                 .get(ClassUtils.resolvePrimitiveIfNecessary(valueClass));
         if(jsonValueConverter == null) {
diff --git a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/JsonParserHelper.java b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/JsonParserHelper.java
index 6c84e8e957..fb20cc1e92 100644
--- a/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/JsonParserHelper.java
+++ b/viewers/restfulobjects/viewer/src/main/java/org/apache/causeway/viewer/restfulobjects/viewer/resources/JsonParserHelper.java
@@ -103,10 +103,10 @@ public class JsonParserHelper {
         if (objectSpec.isValue()) {
             try {
                 return jsonValueEncoder.asAdapter(objectSpec, argValueRepr, null);
-            }catch(IllegalArgumentException ex) {
+            } catch(IllegalArgumentException ex) {
                 argRepr.mapPutString("invalidReason", ex.getMessage());
                 throw ex;
-            }catch(Exception ex) {
+            } catch(Exception ex) {
                 StringBuilder buf = new StringBuilder("Failed to parse representation ");
                 try {
                     final String reprStr = argRepr.getString("value");