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 2020/01/28 11:20:55 UTC

[isis] branch master updated: ISIS-2158: REST-client: also support actions, that return collections

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 51d2ca5  ISIS-2158: REST-client: also support actions, that return collections
51d2ca5 is described below

commit 51d2ca53327c91bc7221f08b2263da30c520cecd
Author: Andi Huber <ah...@apache.org>
AuthorDate: Tue Jan 28 12:20:47 2020 +0100

    ISIS-2158: REST-client: also support actions, that return collections
    
    - previous for single entity result only ... T digest.get()
    - now unified for any cardinality ... Can<T> digest.getEntites()
---
 .../isis/testdomain/jdo/InventoryResource.java     |  20 ++++
 .../isis/testdomain/rest/RestEndpointService.java  |  30 +++++-
 .../isis/testdomain/rest/RestServiceTest.java      |  35 ++++--
 .../shiro/ShiroSecmanLdap_restfulStressTest.java   |   2 +-
 .../isis/extensions/restclient/ResponseDigest.java | 118 +++++++++++++++------
 .../isis/extensions/restclient/RestfulClient.java  |  33 +++++-
 6 files changed, 189 insertions(+), 49 deletions(-)

diff --git a/examples/smoketests/src/main/java/org/apache/isis/testdomain/jdo/InventoryResource.java b/examples/smoketests/src/main/java/org/apache/isis/testdomain/jdo/InventoryResource.java
index f9fb309..220862b 100644
--- a/examples/smoketests/src/main/java/org/apache/isis/testdomain/jdo/InventoryResource.java
+++ b/examples/smoketests/src/main/java/org/apache/isis/testdomain/jdo/InventoryResource.java
@@ -30,8 +30,10 @@ import org.springframework.web.context.request.ServletRequestAttributes;
 import org.apache.isis.applib.annotation.Action;
 import org.apache.isis.applib.annotation.DomainService;
 import org.apache.isis.applib.annotation.NatureOfService;
+import org.apache.isis.applib.annotation.ParameterLayout;
 import org.apache.isis.applib.services.repository.RepositoryService;
 import org.apache.isis.core.commons.internal.base._NullSafe;
+import org.apache.isis.core.commons.internal.collections._Lists;
 
 import lombok.val;
 
@@ -59,6 +61,24 @@ public class InventoryResource {
         return repository.persist(book);
     }
 
+    @Action
+    public List<Book> multipleBooks(
+            
+            @ParameterLayout(named = "")
+            int nrOfBooks
+            
+            ) {
+        
+        val books = _Lists.<Book>newArrayList();
+        
+        // for this test we do not care if we generate duplicates
+        for(int i=0; i<nrOfBooks; ++i) {
+            val book = Book.of("MultipleBooksTest", "An awesome Book["+i+"]", 12, "Author", "ISBN", "Publisher");
+            books.add(repository.persist(book));
+        }
+        return books;
+    }
+    
     @Action //TODO improve the REST client such that the param can be of type Book
     public Book storeBook(String newBook) throws JAXBException { 
         Book book = BookDto.decode(newBook).toBook();
diff --git a/examples/smoketests/src/main/java/org/apache/isis/testdomain/rest/RestEndpointService.java b/examples/smoketests/src/main/java/org/apache/isis/testdomain/rest/RestEndpointService.java
index 5fb1a5e..f494412 100644
--- a/examples/smoketests/src/main/java/org/apache/isis/testdomain/rest/RestEndpointService.java
+++ b/examples/smoketests/src/main/java/org/apache/isis/testdomain/rest/RestEndpointService.java
@@ -18,8 +18,11 @@
  */
 package org.apache.isis.testdomain.rest;
 
+import java.util.List;
+
 import javax.inject.Inject;
 import javax.ws.rs.client.Invocation;
+import javax.ws.rs.core.GenericType;
 import javax.xml.bind.JAXBException;
 
 import org.springframework.core.env.Environment;
@@ -67,11 +70,11 @@ public class RestEndpointService {
 
     public RestfulClient newClient(boolean useRequestDebugLogging) {
 
-        if(useRequestDebugLogging) {
-            final String msg = "RestfulLoggingFilter seems to fail in 4.4.1, consumes the response's entity (in order to log) so that subsequent attempts to read response fail.";
-            log.error(msg);
-            throw new UnsupportedOperationException(msg);
-        }
+//        if(useRequestDebugLogging) {
+//            final String msg = "RestfulLoggingFilter seems to fail in 4.4.1, consumes the response's entity (in order to log) so that subsequent attempts to read response fail.";
+//            log.error(msg);
+//            throw new UnsupportedOperationException(msg);
+//        }
         val restRootPath =
                 String.format("http://localhost:%d%s/",
                         getPort(),
@@ -110,6 +113,23 @@ public class RestEndpointService {
         return digest;
     }
     
+    public ResponseDigest<Book> getMultipleBooks(RestfulClient client) throws JAXBException {
+        
+        Invocation.Builder request = client.request(
+                "services/testdomain.InventoryResource/actions/multipleBooks/invoke",
+                SuppressionType.ALL);
+
+        val args = client.arguments()
+                .addActionParameter("nrOfBooks", 2)
+                .build();
+
+        val response = request.post(args);
+        val digest = client.digestList(response, Book.class, new GenericType<List<Book>>() {});
+
+        return digest;
+    }
+    
+    
     public ResponseDigest<Book> storeBook(RestfulClient client, Book newBook) throws JAXBException {
         val request = client.request(
                 "services/testdomain.InventoryResource/actions/storeBook/invoke", 
diff --git a/examples/smoketests/src/test/java/org/apache/isis/testdomain/rest/RestServiceTest.java b/examples/smoketests/src/test/java/org/apache/isis/testdomain/rest/RestServiceTest.java
index a4245e5..57d6012 100644
--- a/examples/smoketests/src/test/java/org/apache/isis/testdomain/rest/RestServiceTest.java
+++ b/examples/smoketests/src/test/java/org/apache/isis/testdomain/rest/RestServiceTest.java
@@ -58,7 +58,7 @@ class RestServiceTest {
         assertNotNull(restService.getPort());
         assertTrue(restService.getPort()>0);
 
-        val useRequestDebugLogging = false; // not used because seems to fail in 4.4.1, consumes the response's entity and so
+        val useRequestDebugLogging = false;
         val restfulClient = restService.newClient(useRequestDebugLogging);
 
         val digest = restService.getRecommendedBookOfTheWeek(restfulClient);
@@ -67,7 +67,7 @@ class RestServiceTest {
             fail(digest.getFailureCause());
         }
 
-        val bookOfTheWeek = digest.get();
+        val bookOfTheWeek = digest.getEntities().getSingletonOrFail();
 
         assertNotNull(bookOfTheWeek);
         assertEquals("Book of the week", bookOfTheWeek.getName());
@@ -80,7 +80,7 @@ class RestServiceTest {
         assertNotNull(restService.getPort());
         assertTrue(restService.getPort()>0);
 
-        val useRequestDebugLogging = false; // not used because seems to fail in 4.4.1, consumes the response's entity and so
+        val useRequestDebugLogging = false;
         val restfulClient = restService.newClient(useRequestDebugLogging);
 
         val newBook = Book.of("REST Book", "A sample REST book for testing.", 77., 
@@ -92,14 +92,37 @@ class RestServiceTest {
             fail(digest.getFailureCause());
         }
 
-        val storedBook = digest.get();
+        val storedBook = digest.getEntities().getSingletonOrFail();
 
         assertNotNull(storedBook);
         assertEquals("REST Book", storedBook.getName());
 
     }
     
-    
+    @Test
+    void multipleBooks_viaRestEndpoint() throws JAXBException {
+
+        assertNotNull(restService.getPort());
+        assertTrue(restService.getPort()>0);
+
+        val useRequestDebugLogging = false; 
+        val restfulClient = restService.newClient(useRequestDebugLogging);
+
+        val digest = restService.getMultipleBooks(restfulClient);
+
+        if(!digest.isSuccess()) {
+            fail(digest.getFailureCause());
+        }
+
+        val multipleBooks = digest.getEntities();
+        
+        assertEquals(2, multipleBooks.size());
+        
+        for(val book : multipleBooks) {
+            assertEquals("MultipleBooksTest", book.getName());    
+        }
+
+    }
     
     @Test
     void httpSessionInfo() {
@@ -113,7 +136,7 @@ class RestServiceTest {
             fail(digest.getFailureCause());
         }
 
-        val httpSessionInfo = digest.get();
+        val httpSessionInfo = digest.getEntities().getSingletonOrFail();
 
         assertNotNull(httpSessionInfo);
         assertEquals("no http-session", httpSessionInfo);
diff --git a/examples/smoketests/src/test/java/org/apache/isis/testdomain/shiro/ShiroSecmanLdap_restfulStressTest.java b/examples/smoketests/src/test/java/org/apache/isis/testdomain/shiro/ShiroSecmanLdap_restfulStressTest.java
index 3674c78..7899ffd 100644
--- a/examples/smoketests/src/test/java/org/apache/isis/testdomain/shiro/ShiroSecmanLdap_restfulStressTest.java
+++ b/examples/smoketests/src/test/java/org/apache/isis/testdomain/shiro/ShiroSecmanLdap_restfulStressTest.java
@@ -127,7 +127,7 @@ class ShiroSecmanLdap_restfulStressTest extends AbstractShiroTest {
                     fail(digest.getFailureCause());
                 }
                 
-                val httpSessionInfo = digest.get();
+                val httpSessionInfo = digest.getEntities();
 
                 assertNotNull(httpSessionInfo);
                 assertEquals("no http-session", httpSessionInfo);
diff --git a/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/ResponseDigest.java b/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/ResponseDigest.java
index 38b043e..f7dbba8 100644
--- a/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/ResponseDigest.java
+++ b/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/ResponseDigest.java
@@ -21,10 +21,13 @@ package org.apache.isis.extensions.restclient;
 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.concurrent.Future;
 import java.util.function.Function;
 
+import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status.Family;
 
@@ -32,10 +35,12 @@ import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.databind.JsonMappingException;
 
 import org.apache.isis.applib.util.schema.CommonDtoUtils;
+import org.apache.isis.core.commons.collections.Can;
 import org.apache.isis.core.commons.internal.base._Casts;
 import org.apache.isis.core.commons.internal.base._Strings;
 import org.apache.isis.core.commons.internal.resources._Json;
 
+import lombok.NonNull;
 import lombok.val;
 
 /**
@@ -44,16 +49,41 @@ import lombok.val;
  */
 public class ResponseDigest<T> {
 
-    /** synchronous response processing */
-    public static <T> ResponseDigest<T> of(Response response, Class<T> entityType) {
-        return new ResponseDigest<>(response, entityType).digest();
+    /**
+     * synchronous response processing (single entity)
+     * @param <T>
+     * @param response
+     * @param entityType
+     * @return
+     */
+    public static <T> ResponseDigest<T> of(
+            @NonNull final Response response, 
+            @NonNull final Class<T> entityType) {
+        
+        return new ResponseDigest<>(response, entityType, null).digest();
     }
-
+    
+    /**
+     * synchronous response processing (list of entities)
+     * @param <T>
+     * @param response
+     * @param entityType
+     * @param genericType
+     * @return
+     */
+    public static <T> ResponseDigest<T> ofList(
+            @NonNull final Response response, 
+            @NonNull final Class<T> entityType,
+            @NonNull final GenericType<List<T>> genericType) {
+        
+        return new ResponseDigest<>(response, entityType, genericType).digest();
+    }
+    
     /** a-synchronous response failure processing */
     public static <T> ResponseDigest<T> ofAsyncFailure(
-            Future<Response> asyncResponse, 
-            Class<T> entityType, 
-            Exception failure) {
+            final Future<Response> asyncResponse, 
+            final Class<T> entityType, 
+            final Exception failure) {
 
         Response response;
         try {
@@ -62,20 +92,22 @@ public class ResponseDigest<T> {
             response = null;
         }
 
-        final ResponseDigest<T> failureDigest = new ResponseDigest<>(response, entityType);
+        final ResponseDigest<T> failureDigest = new ResponseDigest<>(response, entityType, null);
         return failureDigest.digestAsyncFailure(asyncResponse.isCancelled(), failure);
     }
-
+    
     private final Response response;
     private final Class<T> entityType;
+    private final GenericType<List<T>> genericType;
 
-    private T entity;
+    private Can<T> entities;
     private Exception failureCause;
 
 
-    protected ResponseDigest(Response response, Class<T> entityType) {
+    protected ResponseDigest(Response response, Class<T> entityType, GenericType<List<T>> genericType) {
         this.response = response;
         this.entityType = entityType;
+        this.genericType = genericType;
     }
 
     public boolean isSuccess() {
@@ -86,23 +118,23 @@ public class ResponseDigest<T> {
         return failureCause!=null;
     }
 
-    public T get(){
-        return entity;
+    public Can<T> getEntities(){
+        return entities;
     }
 
     public Exception getFailureCause(){
         return failureCause;
     }
 
-    public T ifSuccessGetOrElseMap(Function<Exception, T> failureMapper) {
+    public Can<T> ifSuccessGetOrElseMap(Function<Exception, Can<T>> failureMapper) {
         return isSuccess() 
-                ? get()
+                ? getEntities()
                         : failureMapper.apply(getFailureCause());
     }
 
-    public <X> X ifSuccessMapOrElseMap(Function<T, X> successMapper, Function<Exception, X> failureMapper) {
+    public <X> X ifSuccessMapOrElseMap(Function<Can<T>, X> successMapper, Function<Exception, X> failureMapper) {
         return isSuccess() 
-                ? successMapper.apply(get())
+                ? successMapper.apply(getEntities())
                         : failureMapper.apply(getFailureCause());
     }
 
@@ -111,47 +143,65 @@ public class ResponseDigest<T> {
     private ResponseDigest<T> digest() {
 
         if(response==null) {
-            entity = null;
+            entities = Can.empty();
             failureCause = new NoSuchElementException();
             return this;
         }
 
         if(!response.hasEntity()) {
-            entity = null;
+            entities = Can.empty();
             failureCause = new NoSuchElementException(defaultFailureMessage(response));
             return this;
         }
 
         if(response.getStatusInfo().getFamily() != Family.SUCCESSFUL) {
-            entity = null;
+            entities = Can.empty();
             failureCause = new RestfulClientException(defaultFailureMessage(response));
             return this;
         }
 
-        if(isValueType(entityType)) {
-            try {
-                val responseBody = response.readEntity(String.class);
-                entity = parseValueTypeBody(responseBody);
-            } catch (Exception e) {
-                entity = null;
-                failureCause = new RestfulClientException("failed to read JAX-RS response content", e);
-            }
-            return this;
-        }
-
         try {
-            entity = response.readEntity(entityType);
+            
+            if(genericType==null) {
+                // when response is a singleton
+                entities = Can.ofSingleton(readSingle());
+            } else {
+                // when response is a list
+                entities = Can.ofCollection(readList());
+            }
+            
         } catch (Exception e) {
-            entity = null;
+            entities = Can.empty();
             failureCause = new RestfulClientException("failed to read JAX-RS response content", e);
         }
 
         return this;
     }
 
+    private T readSingle() throws JsonParseException, JsonMappingException, IOException {
+        if(isValueType(entityType)) {
+            val responseBody = response.readEntity(String.class);
+            return parseValueTypeBody(responseBody);
+        }
+        return response.<T>readEntity(entityType);
+    }
+    
+    private List<T> readList() throws JsonParseException, JsonMappingException, IOException {
+        if(isValueType(entityType)) {
+            final List<String> valueBodies = response.readEntity(new GenericType<List<String>>() {});
+            final List<T> resultList = new ArrayList<>(valueBodies.size());
+            for(val valueBody : valueBodies) {
+                // explicit loop, for simpler exception propagation
+                resultList.add(parseValueTypeBody(valueBody)); 
+            }
+            return resultList;
+        }
+        return response.readEntity(genericType);
+    }
+
     private ResponseDigest<T> digestAsyncFailure(boolean isCancelled, Exception failure) {
 
-        entity = null;
+        entities = Can.empty();
 
         if(isCancelled) {
             failureCause = new RestfulClientException("Async JAX-RS request was canceled", failure);
diff --git a/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/RestfulClient.java b/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/RestfulClient.java
index 039d020..65c18d1 100644
--- a/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/RestfulClient.java
+++ b/mappings/restclient/api/src/main/java/org/apache/isis/extensions/restclient/RestfulClient.java
@@ -19,6 +19,7 @@
 package org.apache.isis.extensions.restclient;
 
 import java.util.EnumSet;
+import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
 import java.util.stream.Collectors;
@@ -26,6 +27,7 @@ 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 org.apache.isis.applib.client.SuppressionType;
@@ -71,7 +73,7 @@ ResponseDigest<MyObject> digest = client.digest(response, MyObject.class);
 }
 
 if(digest.isSuccess()) {
-    System.out.println("result: "+ digest.get().get$$instanceId());
+    System.out.println("result: "+ digest.getEntities().getSingletonOrFail().get$$instanceId());
 } else {
     digest.getFailureCause().printStackTrace();
 }
@@ -97,7 +99,7 @@ ResponseDigest<MyObject> digest = digestFuture.get(); // blocking
 }
 
 if(digest.isSuccess()) {
-    System.out.println("result: "+ digest.get().get$$instanceId());
+    System.out.println("result: "+ digest.getEntities().getSingletonOrFail().get$$instanceId());
 } else {
     digest.getFailureCause().printStackTrace();
 }
@@ -185,6 +187,10 @@ public class RestfulClient {
         return ResponseDigest.of(response, entityType);
     }
 
+    public <T> ResponseDigest<T> digestList(Response response, Class<T> entityType, GenericType<List<T>> genericType) {
+        return ResponseDigest.ofList(response, entityType, genericType);
+    }
+    
     // -- RESPONSE PROCESSING (ASYNC)
 
     public <T> CompletableFuture<ResponseDigest<T>> digest(
@@ -205,6 +211,28 @@ public class RestfulClient {
 
         return completableFuture;
     }
+    
+    public <T> CompletableFuture<ResponseDigest<T>> digestList(
+            final Future<Response> asyncResponse, 
+            final Class<T> entityType,
+            GenericType<List<T>> genericType) {
+
+        final CompletableFuture<ResponseDigest<T>> completableFuture = CompletableFuture.supplyAsync(()->{
+            try {
+                Response response = asyncResponse.get();
+                ResponseDigest<T> digest = digestList(response, entityType, genericType);
+
+                return digest;
+
+            } catch (Exception e) {
+                
+                return ResponseDigest.ofAsyncFailure(asyncResponse, entityType, e);
+                
+            }
+        });
+
+        return completableFuture;
+    }
 
     // -- FILTER
 
@@ -258,5 +286,4 @@ public class RestfulClient {
 
 
 
-
 }