You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2022/12/01 10:37:58 UTC

[isis] branch master updated: ISIS-3275: re-implement OutboxClient on top of RestClient (4)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new b7076bfafa ISIS-3275: re-implement OutboxClient on top of RestClient (4)
b7076bfafa is described below

commit b7076bfafaa8709670aa06de8be23e2a481d10c6
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Dec 1 11:37:49 2022 +0100

    ISIS-3275: re-implement OutboxClient on top of RestClient (4)
    
    - code de-duplication
---
 .../causeway/applib/client/SuppressionType.java    |   8 +-
 .../restclient/api/OutboxClient.java               | 101 +++++-----------
 .../secondary/fetch/CommandFetcher.java            |   7 +-
 .../testdomain/util/rest/RestEndpointService.java  |   4 +-
 .../restfulobjects/client/ResponseDigest.java      |  66 ++---------
 .../restfulobjects/client/ResponseDigester.java    | 127 +++++++++++++++++++++
 .../restfulobjects/client/RestfulClient.java       |  64 +++++------
 .../client/RestfulClientMediaType.java             |  71 ++++++++++++
 8 files changed, 276 insertions(+), 172 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/client/SuppressionType.java b/api/applib/src/main/java/org/apache/causeway/applib/client/SuppressionType.java
index 8dcb94359a..889ded6a97 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/client/SuppressionType.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/client/SuppressionType.java
@@ -62,7 +62,9 @@ public enum SuppressionType {
 
     ;
 
-    public static EnumSet<SuppressionType> setOf(SuppressionType ... types){
+    public static EnumSet<SuppressionType> all() { return EnumSet.of(ALL); };
+
+    public static EnumSet<SuppressionType> setOf(final SuppressionType ... types){
         final EnumSet<SuppressionType> set = EnumSet.noneOf(SuppressionType.class);
         stream(types).forEach(set::add);
         return set;
@@ -70,7 +72,7 @@ public enum SuppressionType {
 
     public static class ParseUtil {
 
-        public static EnumSet<SuppressionType> parse(List<String> parameterList) {
+        public static EnumSet<SuppressionType> parse(final List<String> parameterList) {
             final EnumSet<SuppressionType> set = EnumSet.noneOf(SuppressionType.class);
             parameterList.stream()
             .map(SuppressionType.ParseUtil::parseOrElseNull)
@@ -82,7 +84,7 @@ public enum SuppressionType {
             return set;
         }
 
-        private static SuppressionType parseOrElseNull(String literal) {
+        private static SuppressionType parseOrElseNull(final String literal) {
 
             // honor pre v2 behavior
             if("true".equalsIgnoreCase(literal)) {
diff --git a/extensions/core/executionoutbox/restclient/src/main/java/org/apache/causeway/extensions/executionoutbox/restclient/api/OutboxClient.java b/extensions/core/executionoutbox/restclient/src/main/java/org/apache/causeway/extensions/executionoutbox/restclient/api/OutboxClient.java
index 12b0715db7..53d8c926a6 100644
--- a/extensions/core/executionoutbox/restclient/src/main/java/org/apache/causeway/extensions/executionoutbox/restclient/api/OutboxClient.java
+++ b/extensions/core/executionoutbox/restclient/src/main/java/org/apache/causeway/extensions/executionoutbox/restclient/api/OutboxClient.java
@@ -21,15 +21,14 @@
 package org.apache.causeway.extensions.executionoutbox.restclient.api;
 
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 
-import javax.ws.rs.client.Client;
 import javax.ws.rs.client.Entity;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.UriBuilder;
 
 import org.apache.causeway.applib.util.schema.InteractionsDtoUtils;
+import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.internal.resources._Json;
 import org.apache.causeway.extensions.executionoutbox.restclient.api.delete.DeleteMessage;
 import org.apache.causeway.extensions.executionoutbox.restclient.api.deleteMany.DeleteManyMessage;
@@ -41,6 +40,7 @@ import org.apache.causeway.schema.ixn.v2.MemberExecutionDto;
 import org.apache.causeway.schema.ixn.v2.PropertyEditDto;
 import org.apache.causeway.viewer.restfulobjects.client.RestfulClient;
 import org.apache.causeway.viewer.restfulobjects.client.RestfulClientConfig;
+import org.apache.causeway.viewer.restfulobjects.client.RestfulClientMediaType;
 
 import lombok.Setter;
 import lombok.val;
@@ -89,7 +89,6 @@ public class OutboxClient {
         return this;
     }
 
-    private UriBuilder pendingUriBuilder;
     private UriBuilder deleteUriBuilder;
     private UriBuilder deleteManyUriBuilder;
 
@@ -103,7 +102,6 @@ public class OutboxClient {
      * Should be called once all properties have been injected.
      */
     public void init() {
-        this.pendingUriBuilder = UriBuilder.fromUri(base + "services/causeway.ext.executionOutbox.OutboxRestApi/actions/pending/invoke");
         this.deleteUriBuilder = UriBuilder.fromUri(base + "services/causeway.ext.executionOutbox.OutboxRestApi/actions/delete/invoke");
         this.deleteManyUriBuilder = UriBuilder.fromUri(base + "services/causeway.ext.executionOutbox.OutboxRestApi/actions/deleteMany/invoke");
 
@@ -112,7 +110,8 @@ public class OutboxClient {
         restfulClientConfig.setRestfulAuthUser(username);
         restfulClientConfig.setRestfulAuthPassword(password);
         restfulClientConfig.setConnectTimeoutInMillis(1000L * connectTimeoutInSecs);
-        restfulClientConfig.setReadTimeoutInMillis(1000L * connectTimeoutInSecs);
+        restfulClientConfig.setReadTimeoutInMillis(1000L * readTimeoutInSecs);
+        //restfulClientConfig.setUseRequestDebugLogging(true); //for debugging
     }
 
     private void ensureInitialized() {
@@ -125,65 +124,47 @@ public class OutboxClient {
 
         ensureInitialized();
 
-        val uri = pendingUriBuilder.build();
+        try(val client = RestfulClient.ofConfig(restfulClientConfig)) {
 
-        Client client = null;
-        try {
-            client = RestfulClient.ofConfig(restfulClientConfig).getJaxRsClient();
+            var response = client.request(PENDING_URI)
+                    .accept(RestfulClientMediaType.RO_XML.mediaTypeFor(InteractionsDto.class))
+                    .get();
 
-            val webTarget = client.target(uri);
+            final Try<InteractionsDto> digest = client.digest(response, InteractionsDto.class);
 
-            val invocationBuilder = webTarget.request()
-                    .header("Authorization", "Basic " + encode(username, password))
-                    .accept(mediaTypeFor(InteractionsDto.class))
-                    ;
-
-            val invocation = invocationBuilder.buildGet();
-            val response = invocation.invoke();
-
-            val responseStatus = response.getStatus();
-            if (responseStatus != 200) {
-                log.warn(invocation.toString());
+            if(digest.isSuccess()) {
+                return digest.getValue()
+                        .map(InteractionsDto::getInteractionDto)
+                        .orElseGet(Collections::emptyList);
+            } else {
+                log.error("Failed to GET from {}: {}", client.uri(PENDING_URI), digest.getFailure().get());
+                return Collections.emptyList();
             }
-
-            final InteractionsDto interactionsDto = response.readEntity(InteractionsDto.class);
-            return interactionsDto.getInteractionDto();
-
-        } catch(Exception ex) {
-            log.error(String.format("Failed to GET from %s", uri.toString()), ex);
-        } finally {
-            closeQuietly(client);
         }
-        return Collections.emptyList();
-    }
-
-    // -- HELPER
 
-    private static MediaType mediaTypeFor(final Class<?> dtoClass) {
-
-        val headers = new HashMap<String,String>();
-        headers.put("profile", "urn:org.restfulobjects:repr-types/action-result");
-        headers.put("x-ro-domain-type", dtoClass.getName());
-        return new MediaType("application", "xml", headers);
     }
 
-
     public void delete(final String interactionId, final int sequence) {
         val entity = new DeleteMessage(interactionId, sequence);
-        invoke(entity, deleteUriBuilder);
+        invoke(entity, DELETE_URI);
     }
 
     public void deleteMany(final List<InteractionDto> interactionDtos) {
-
         val interactionsDto = new InteractionsDto();
         interactionDtos.forEach(interactionDto -> {
             addTo(interactionsDto, interactionDto);
         });
 
         val entity = new DeleteManyMessage(InteractionsDtoUtils.toXml(interactionsDto));
-        invoke(entity, deleteManyUriBuilder);
+        invoke(entity, DELETE_MANY_URI);
     }
 
+    // -- HELPER
+
+    private static String PENDING_URI = "services/causeway.ext.executionOutbox.OutboxRestApi/actions/pending/invoke";
+    private static String DELETE_URI = "services/causeway.ext.executionOutbox.OutboxRestApi/actions/delete/invoke";
+    private static String DELETE_MANY_URI = "services/causeway.ext.executionOutbox.OutboxRestApi/actions/deleteMany/invoke";
+
     private void addTo(final InteractionsDto interactionsDto, final InteractionDto orig) {
         val copy = new InteractionDto();
         copy.setInteractionId(orig.getInteractionId());
@@ -204,20 +185,15 @@ public class OutboxClient {
                 : new PropertyEditDto();
     }
 
-    private void invoke(final Object entity, final UriBuilder uriBuilder) {
+    private void invoke(final Object entity, final String path) {
 
         ensureInitialized();
 
         val json =  _Json.toString(entity);
 
-        Client client = null;
-        try {
-            client = RestfulClient.ofConfig(restfulClientConfig).getJaxRsClient();
+        try(val client = RestfulClient.ofConfig(restfulClientConfig)) {
 
-            val webTarget = client.target(uriBuilder.build());
-
-            val invocationBuilder = webTarget.request();
-            invocationBuilder.header("Authorization", "Basic " + encode(username, password));
+            var invocationBuilder = client.request(path);
 
             val invocation = invocationBuilder.buildPut(
                     Entity.entity(json, MediaType.APPLICATION_JSON_TYPE));
@@ -229,31 +205,8 @@ public class OutboxClient {
                 // if failed to log message via REST service, then fallback by logging to slf4j
                 log.warn(entity.toString());
             }
-        } catch(Exception ex) {
-            log.error(entity.toString(), ex);
-        } finally {
-            closeQuietly(client);
         }
-    }
-
-    private static String encode(final String username, final String password) {
-        return java.util.Base64.getEncoder().encodeToString(asBytes(username, password));
-    }
-
-    private static byte[] asBytes(final String username, final String password) {
-        return String.format("%s:%s", username, password).getBytes();
-    }
 
-    private static void closeQuietly(final Client client) {
-        if (client == null) {
-            return;
-        }
-        try {
-            client.close();
-        } catch (Exception ex) {
-            // ignore so as to avoid overriding any pending exceptions in calling 'finally' block.
-        }
     }
 
-
 }
diff --git a/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/fetch/CommandFetcher.java b/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/fetch/CommandFetcher.java
index 5c9817651d..05cbe3cd47 100644
--- a/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/fetch/CommandFetcher.java
+++ b/incubator/extensions/core/commandreplay/secondary/src/main/java/org/apache/causeway/extensions/commandreplay/secondary/fetch/CommandFetcher.java
@@ -18,6 +18,7 @@
  */
 package org.apache.causeway.extensions.commandreplay.secondary.fetch;
 
+import java.util.EnumSet;
 import java.util.List;
 import java.util.UUID;
 
@@ -40,6 +41,7 @@ import org.apache.causeway.extensions.commandreplay.secondary.status.StatusExcep
 import org.apache.causeway.schema.cmd.v2.CommandDto;
 import org.apache.causeway.viewer.restfulobjects.client.RestfulClient;
 import org.apache.causeway.viewer.restfulobjects.client.RestfulClientConfig;
+import org.apache.causeway.viewer.restfulobjects.client.RestfulClientMediaType;
 
 import lombok.AccessLevel;
 import lombok.RequiredArgsConstructor;
@@ -107,9 +109,8 @@ public class CommandFetcher {
     Can<CommandDto> callPrimary(final @Nullable UUID interactionId) throws StatusException {
 
         val client = newClient(secondaryConfig, useRequestDebugLogging);
-        val request = client.request(
-                URL_SUFFIX,
-                SuppressionType.RO);
+        val request = client.request(URL_SUFFIX)
+                .accept(RestfulClientMediaType.SIMPLE_JSON.mediaTypeFor(CommandDto.class, EnumSet.of(SuppressionType.RO)));
 
         val args = client.arguments()
                 .addActionParameter("interactionId", interactionId!=null ? interactionId.toString() : null)
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 aa295008bb..f8d2ca8ae0 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
@@ -41,6 +41,7 @@ import org.apache.causeway.testdomain.ldap.LdapConstants;
 import org.apache.causeway.testdomain.util.dto.BookDto;
 import org.apache.causeway.viewer.restfulobjects.client.RestfulClient;
 import org.apache.causeway.viewer.restfulobjects.client.RestfulClientConfig;
+import org.apache.causeway.viewer.restfulobjects.client.RestfulClientMediaType;
 import org.apache.causeway.viewer.restfulobjects.client.log.ClientConversationFilter;
 
 import lombok.NonNull;
@@ -107,7 +108,8 @@ public class RestEndpointService {
     // -- NEW REQUEST BUILDER
 
     public Invocation.Builder newInvocationBuilder(final RestfulClient client, final String endpointPath) {
-        return client.request(endpointPath, SuppressionType.ALL);
+        return client.request(endpointPath)
+                .accept(RestfulClientMediaType.SIMPLE_JSON.mediaTypeFor(Object.class, SuppressionType.all()));
     }
 
     // -- ENDPOINTS
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 2a681cd1bf..5c4ef3c978 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
@@ -18,10 +18,8 @@
  */
 package org.apache.causeway.viewer.restfulobjects.client;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Optional;
@@ -30,18 +28,11 @@ import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status.Family;
 
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 import org.springframework.lang.Nullable;
 
-import org.apache.causeway.applib.client.RepresentationTypeSimplifiedV2;
 import org.apache.causeway.commons.collections.Can;
-import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.viewer.restfulobjects.applib.dtos.ScalarValueDtoV2;
 
 import lombok.NonNull;
 import lombok.val;
@@ -158,9 +149,9 @@ class ResponseDigest<T> {
 
         // see if we can extract the returned representation type (repr-type) from the header
         val contentTypeHeaderString = response.getHeaderString("Content-Type");
-        val reprType = RepresentationTypeSimplifiedV2.parseContentTypeHeaderString(contentTypeHeaderString)
-                .orElse(null);
-        if(reprType==null) {
+
+        val digester = ResponseDigester.forContentTypeHeaderString(contentTypeHeaderString).orElse(null);
+        if(digester==null) {
             entities = Can.empty();
             failureCause = _Exceptions.unrecoverable(String.format(
                     "Invalid REST response, cannot parse header's Content-Type '%s' for the repr-type to use",
@@ -172,13 +163,15 @@ class ResponseDigest<T> {
 
             if(genericType==null) {
                 // when response is a singleton
-                val singleton = readSingle(reprType);
+                log.debug("readSingle({})", digester);
+                val singleton = digester.readSingle(entityType, response);
                 entities = singleton==null
                         ? Can.empty()
                         : Can.ofSingleton(singleton);
             } else {
                 // when response is a list
-                entities = Can.ofCollection(readList(reprType));
+                log.debug("readList({})", digester);
+                entities = Can.ofCollection(digester.readList(entityType, genericType, response));
             }
 
         } catch (Exception e) {
@@ -189,46 +182,6 @@ class ResponseDigest<T> {
         return this;
     }
 
-    private T readSingle(final RepresentationTypeSimplifiedV2 reprType)
-            throws JsonParseException, JsonMappingException, IOException {
-
-        log.debug("readSingle({})", reprType);
-
-        if(reprType.isValue()
-                || reprType.isValues()) {
-            val mapper = new ObjectMapper();
-            val jsonInput = response.readEntity(String.class);
-            val scalarValueDto = mapper.readValue(jsonInput, ScalarValueDtoV2.class);
-            return extractValue(scalarValueDto);
-        }
-        return response.<T>readEntity(entityType);
-    }
-
-    private List<T> readList(final RepresentationTypeSimplifiedV2 reprType)
-            throws JsonParseException, JsonMappingException, IOException {
-
-        log.debug("readList({})", reprType);
-
-        if(reprType.isValues()
-                || reprType.isValue()) {
-            val mapper = new ObjectMapper();
-            val jsonInput = response.readEntity(String.class);
-            final List<ScalarValueDtoV2> scalarValueDtoList =
-                    mapper.readValue(
-                            jsonInput,
-                            mapper.getTypeFactory().constructCollectionType(List.class, ScalarValueDtoV2.class));
-
-            final List<T> resultList = new ArrayList<>(scalarValueDtoList.size());
-            for(val valueBody : scalarValueDtoList) {
-                // explicit loop, for simpler exception propagation
-                resultList.add(extractValue(valueBody));
-            }
-            return resultList;
-
-        }
-        return response.readEntity(genericType);
-    }
-
     private String defaultFailureMessage(final Response response) {
         String failureMessage = "non-successful JAX-RS response: " +
                 String.format("%s (Http-Status-Code: %d)",
@@ -247,12 +200,7 @@ class ResponseDigest<T> {
         return failureMessage;
     }
 
-    // -- VALUE TYPE HANDLING
 
-    private T extractValue(final ScalarValueDtoV2 scalarValueDto)
-            throws JsonParseException, JsonMappingException, IOException {
-        return _Casts.uncheckedCast(scalarValueDto.getValue());
-    }
 
 
 }
diff --git a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigester.java b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigester.java
new file mode 100644
index 0000000000..ed4c52651d
--- /dev/null
+++ b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/ResponseDigester.java
@@ -0,0 +1,127 @@
+/*
+ *  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.client;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.lang.Nullable;
+
+import org.apache.causeway.applib.client.RepresentationTypeSimplifiedV2;
+import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.viewer.restfulobjects.applib.dtos.ScalarValueDtoV2;
+
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.val;
+
+interface ResponseDigester {
+
+    <T> T readSingle(Class<T> entityType, Response response);
+    <T> List<T> readList(Class<T> entityType, GenericType<List<T>> genericType, Response response);
+
+    // -- FACTORIES
+
+    static Optional<ResponseDigester> forContentTypeHeaderString(final @Nullable String contentTypeHeaderString) {
+        if(_Strings.isEmpty(contentTypeHeaderString)) {
+            return Optional.empty();
+        }
+        if(contentTypeHeaderString.startsWith("application/xml;profile=\"urn:org.restfulobjects:repr-types/action-result\"")
+                && contentTypeHeaderString.contains("x-ro-domain-type")) {
+            return Optional.of(new ResponseDigesterXmlStandard());
+        }
+        return RepresentationTypeSimplifiedV2.parseContentTypeHeaderString(contentTypeHeaderString)
+              .map(ResponseDigesterJsonSimple::new);
+    }
+
+    // -- IMPLEMENTATIONS
+
+    @RequiredArgsConstructor
+    static class ResponseDigesterXmlStandard implements ResponseDigester {
+
+        @Override
+        public <T> T readSingle(final Class<T> entityType, final Response response) {
+            return response.readEntity(entityType);
+        }
+
+        @Override
+        public <T> List<T> readList(final Class<T> entityType, final GenericType<List<T>> genericType, final Response response) {
+            throw _Exceptions.notImplemented();
+        }
+    }
+
+    @RequiredArgsConstructor
+    static class ResponseDigesterJsonSimple implements ResponseDigester {
+
+        private final RepresentationTypeSimplifiedV2 reprType;
+
+        @SneakyThrows
+        @Override
+        public <T> T readSingle(final Class<T> entityType, final Response response) {
+            if(reprType.isValue()
+                    || reprType.isValues()) {
+                val mapper = new ObjectMapper();
+                val jsonInput = response.readEntity(String.class);
+                val scalarValueDto = mapper.readValue(jsonInput, ScalarValueDtoV2.class);
+                return extractValue(scalarValueDto);
+            }
+            return response.<T>readEntity(entityType);
+        }
+
+        @SneakyThrows
+        @Override
+        public <T> List<T> readList(final Class<T> entityType, final GenericType<List<T>> genericType, final Response response) {
+            if(reprType.isValues()
+                    || reprType.isValue()) {
+                val mapper = new ObjectMapper();
+                val jsonInput = response.readEntity(String.class);
+                final List<ScalarValueDtoV2> scalarValueDtoList =
+                        mapper.readValue(
+                                jsonInput,
+                                mapper.getTypeFactory().constructCollectionType(List.class, ScalarValueDtoV2.class));
+
+                final List<T> resultList = new ArrayList<>(scalarValueDtoList.size());
+                for(val valueBody : scalarValueDtoList) {
+                    // explicit loop, for simpler exception propagation
+                    resultList.add(extractValue(valueBody));
+                }
+                return resultList;
+
+            }
+            return response.readEntity(genericType);
+        }
+
+        private <T> T extractValue(final ScalarValueDtoV2 scalarValueDto)
+                throws JsonParseException, JsonMappingException, IOException {
+            return _Casts.uncheckedCast(scalarValueDto.getValue());
+        }
+    }
+
+}
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 d5b5815016..66a1bd38f0 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
@@ -18,22 +18,20 @@
  */
 package org.apache.causeway.viewer.restfulobjects.client;
 
-import java.util.EnumSet;
+import java.net.URI;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
 
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.client.Invocation.Builder;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
 
-import org.apache.causeway.applib.client.SuppressionType;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.Try;
-import org.apache.causeway.commons.internal.base._NullSafe;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.context._Context;
 import org.apache.causeway.viewer.restfulobjects.client.auth.BasicAuthFilter;
@@ -90,9 +88,7 @@ if(digest.isSuccess()) {
  * @since 2.0 {@index}
  */
 @Log4j2
-public class RestfulClient {
-
-    private static final String DEFAULT_RESPONSE_CONTENT_TYPE = "application/json;profile=\"urn:org.apache.causeway/v2\"";
+public class RestfulClient implements AutoCloseable {
 
     private RestfulClientConfig clientConfig;
     private Client client;
@@ -129,17 +125,24 @@ public class RestfulClient {
         return client;
     }
 
-    // -- REQUEST BUILDER
-
-    public Builder request(final String path, final SuppressionType ... suppressionTypes) {
-        return request(path, SuppressionType.setOf(suppressionTypes));
+    @Override
+    public void close() {
+        if (client == null) {
+            return;
+        }
+        try {
+            client.close();
+        } catch (Throwable ex) {
+            // just ignore
+        }
     }
 
-    public Builder request(final String path, final EnumSet<SuppressionType> suppressionTypes) {
-        final String responseContentType = DEFAULT_RESPONSE_CONTENT_TYPE
-                + toSuppressionLiteral(suppressionTypes);
+    // -- REQUEST BUILDER
 
-        return client.target(relativePathToUri(path)).request(responseContentType);
+    public Builder request(final String path) {
+        return client
+                .target(relativePathToUri(path))
+                .request();
     }
 
     // -- ARGUMENT BUILDER
@@ -166,7 +169,17 @@ public class RestfulClient {
         return Try.failure(listDigest.getFailureCause());
     }
 
-    // -- FILTER
+    // -- UTILITY
+
+    /**
+     * Returns an {@link URI} constructed from this client's base path plus given relative {@code path}.
+     * @param path relative to this client's base
+     */
+    public URI uri(final String path) {
+        return relativePathToUri(path).build();
+    }
+
+    // -- HELPER FILTER
 
     private void registerDefaultJsonProvider() {
         try {
@@ -197,27 +210,14 @@ public class RestfulClient {
         .forEach(client::register);
     }
 
-    // -- HELPER
+    // -- HELPER OTHER
 
-    private String relativePathToUri(String path) {
+    private UriBuilder relativePathToUri(String path) {
         final String baseUri = _Strings.suffix(clientConfig.getRestfulBase(), "/");
         while(path.startsWith("/")) {
             path = path.substring(1);
         }
-        return baseUri + path;
-    }
-
-    private String toSuppressionLiteral(final EnumSet<SuppressionType> suppressionTypes) {
-        final String suppressionSetLiteral = _NullSafe.stream(suppressionTypes)
-                .map(SuppressionType::name)
-                .collect(Collectors.joining(","));
-
-        if(_Strings.isNotEmpty(suppressionSetLiteral)) {
-            return ";suppress=" + suppressionSetLiteral;
-        }
-
-        return "";
+        return UriBuilder.fromUri(baseUri + path);
     }
 
-
 }
diff --git a/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClientMediaType.java b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClientMediaType.java
new file mode 100644
index 0000000000..30f6eefd3c
--- /dev/null
+++ b/viewers/restfulobjects/client/src/main/java/org/apache/causeway/viewer/restfulobjects/client/RestfulClientMediaType.java
@@ -0,0 +1,71 @@
+/*
+ *  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.client;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.core.MediaType;
+
+import org.apache.causeway.applib.client.SuppressionType;
+import org.apache.causeway.commons.internal.base._NullSafe;
+import org.apache.causeway.commons.internal.base._Strings;
+
+public enum RestfulClientMediaType {
+    RO_XML{
+        @Override
+        public MediaType mediaTypeFor(final Class<?> dtoClass, final EnumSet<SuppressionType> suppressionTypes) {
+            return new MediaType("application", "xml",
+                    Map.<String, String>of(
+                            "profile", "urn:org.restfulobjects:repr-types/action-result"
+                                    + toSuppressionLiteral(suppressionTypes),
+                            "x-ro-domain-type", dtoClass.getName()));
+        }
+    },
+    SIMPLE_JSON {
+        @Override
+        public MediaType mediaTypeFor(final Class<?> dtoClass, final EnumSet<SuppressionType> suppressionTypes) {
+            return new MediaType("application", "json",
+                    Map.<String, String>of(
+                            "profile", "urn:org.apache.causeway/v2"
+                                    + toSuppressionLiteral(suppressionTypes),
+                            "x-ro-domain-type", dtoClass.getName()));
+        }
+    }
+    ;
+
+    public final MediaType mediaTypeFor(final Class<?> dtoClass) {
+        return mediaTypeFor(dtoClass, EnumSet.noneOf(SuppressionType.class));
+    }
+
+    public abstract MediaType mediaTypeFor(final Class<?> dtoClass, EnumSet<SuppressionType> suppressionTypes);
+
+    private static String toSuppressionLiteral(final EnumSet<SuppressionType> suppressionTypes) {
+        final String suppressionSetLiteral = _NullSafe.stream(suppressionTypes)
+                .map(SuppressionType::name)
+                .collect(Collectors.joining(","));
+        if(_Strings.isNotEmpty(suppressionSetLiteral)) {
+            return ";suppress=" + suppressionSetLiteral;
+        }
+        return "";
+    }
+
+}
+