Proper handling of chunked input streams in LoggingInterceptor (#4753)

Signed-off-by: Denis Kurochkin <d.k.brazz@gmail.com>
diff --git a/core-common/pom.xml b/core-common/pom.xml
index 3288c5f..e36288f 100644
--- a/core-common/pom.xml
+++ b/core-common/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-    Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
 
     This program and the accompanying materials are made available under the
     terms of the Eclipse Public License v. 2.0, which is available at
@@ -211,6 +211,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.hamcrest</groupId>
             <artifactId>hamcrest-library</artifactId>
             <scope>test</scope>
diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java
index c90d8b6..7ad26ec 100644
--- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java
+++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -193,7 +193,16 @@
         }
         stream.mark(maxEntitySize + 1);
         final byte[] entity = new byte[maxEntitySize + 1];
-        final int entitySize = stream.read(entity);
+
+        int entitySize = 0;
+        while (entitySize < entity.length) {
+            int readBytes = stream.read(entity, entitySize, entity.length - entitySize);
+            if (readBytes < 0) {
+                break;
+            }
+            entitySize += readBytes;
+        }
+
         b.append(new String(entity, 0, Math.min(entitySize, maxEntitySize), charset));
         if (entitySize > maxEntitySize) {
             b.append("...more...");
diff --git a/core-common/src/test/java/org/glassfish/jersey/logging/LoggingInterceptorTest.java b/core-common/src/test/java/org/glassfish/jersey/logging/LoggingInterceptorTest.java
index e4e5e91..5516273 100644
--- a/core-common/src/test/java/org/glassfish/jersey/logging/LoggingInterceptorTest.java
+++ b/core-common/src/test/java/org/glassfish/jersey/logging/LoggingInterceptorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,14 +16,27 @@
 
 package org.glassfish.jersey.logging;
 
+import org.mockito.stubbing.Answer;
+
 import javax.ws.rs.core.MediaType;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Random;
 
 import org.junit.Test;
 import static org.glassfish.jersey.logging.LoggingFeature.Verbosity.HEADERS_ONLY;
 import static org.glassfish.jersey.logging.LoggingFeature.Verbosity.PAYLOAD_ANY;
 import static org.glassfish.jersey.logging.LoggingFeature.Verbosity.PAYLOAD_TEXT;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
 import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE;
@@ -106,4 +119,67 @@
         assertFalse(LoggingInterceptor.printEntity(HEADERS_ONLY, APPLICATION_OCTET_STREAM_TYPE));
     }
 
+    //
+    // logInboundEntity
+    //
+
+    @Test
+    public void testLogInboundEntityMockedStream() throws Exception {
+        int maxEntitySize = 20;
+        LoggingInterceptor loggingInterceptor = new LoggingInterceptor(null, null, null, maxEntitySize) {};
+
+        StringBuilder buffer = new StringBuilder();
+        InputStream stream = mock(InputStream.class);
+        when(stream.markSupported()).thenReturn(true);
+
+        when(stream.read(any(), eq(0), eq(maxEntitySize + 1)))
+                .thenAnswer(chunk(4, 'a'));
+        when(stream.read(any(), eq(4), eq(maxEntitySize + 1 - 4)))
+                .thenAnswer(chunk(3, 'b'));
+        when(stream.read(any(), eq(7), eq(maxEntitySize + 1 - 7)))
+                .thenAnswer(chunk(5, 'c'));
+        when(stream.read(any(), eq(12), eq(maxEntitySize + 1 - 12)))
+                .thenReturn(-1);
+
+        loggingInterceptor.logInboundEntity(buffer, stream, StandardCharsets.UTF_8);
+
+        assertEquals("aaaabbbccccc\n", buffer.toString());
+        verify(stream).mark(maxEntitySize + 1);
+        verify(stream).reset();
+    }
+
+    private Answer<?> chunk(int size, char filler) {
+        return invocation -> {
+            byte[] buf = invocation.getArgumentAt(0, byte[].class);
+            int offset = invocation.getArgumentAt(1, Integer.class);
+            Arrays.fill(buf, offset, offset + size, (byte) filler);
+            return size;
+        };
+    }
+
+    @Test
+    public void testLogInboundEntityRealStream() throws Exception {
+        int maxEntitySize = 2000;
+        String inputString = getRandomString(maxEntitySize * 2);
+
+        LoggingInterceptor loggingInterceptor = new LoggingInterceptor(null, null, null, maxEntitySize) {};
+        StringBuilder buffer = new StringBuilder();
+        InputStream stream = new ByteArrayInputStream(inputString.getBytes());
+
+        loggingInterceptor.logInboundEntity(buffer, stream, StandardCharsets.UTF_8);
+
+        assertEquals(inputString.substring(0, maxEntitySize) + "...more...\n", buffer.toString());
+    }
+
+    private static String getRandomString(int length) {
+        final String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890 _";
+        StringBuilder result = new StringBuilder();
+
+        while (length > 0) {
+            Random rand = new Random();
+            result.append(characters.charAt(rand.nextInt(characters.length())));
+            length--;
+        }
+        return result.toString();
+    }
 }
diff --git a/core-common/src/test/resources/surefire.policy b/core-common/src/test/resources/surefire.policy
index 27602ae..530db3c 100644
--- a/core-common/src/test/resources/surefire.policy
+++ b/core-common/src/test/resources/surefire.policy
@@ -29,12 +29,15 @@
 grant codebase "file:${project.build.directory}/test-classes/-" {
   permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
   permission java.lang.RuntimePermission "modifyThread";
-  permission java.util.PropertyPermission "*", "write";
+  permission java.util.PropertyPermission "*", "read,write";
   permission java.io.FilePermission "${java.io.tmpdir}/-", "read,write,delete";
   permission java.lang.RuntimePermission "getClassLoader";
   permission java.lang.RuntimePermission "accessClassInPackage.sun.misc";
   permission java.lang.RuntimePermission "accessClassInPackage.sun.misc.*";
   permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
+  permission java.lang.RuntimePermission "accessDeclaredMembers";
+  permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect";
+  permission java.lang.RuntimePermission "reflectionFactoryAccess";
 };
 
 grant codebase "file:${project.build.directory}/classes/-" {