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:07 UTC

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

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");