PROTON-2297 Fix issue with composite buffer utf8 decode

Handle multi-byte encoding that cross composite array boundaries and add
some tests.
diff --git a/proton-j/src/main/java/org/apache/qpid/proton/codec/CompositeReadableBuffer.java b/proton-j/src/main/java/org/apache/qpid/proton/codec/CompositeReadableBuffer.java
index abbb40b..a4c6de9 100644
--- a/proton-j/src/main/java/org/apache/qpid/proton/codec/CompositeReadableBuffer.java
+++ b/proton-j/src/main/java/org/apache/qpid/proton/codec/CompositeReadableBuffer.java
@@ -578,12 +578,27 @@
         do {
             boolean endOfInput = processed == viewSpan;
             step = decoder.decode(wrapper, decoded, endOfInput);
-            if (step.isUnderflow() && endOfInput) {
-                step = decoder.flush(decoded);
-                break;
-            }
 
-            if (step.isOverflow()) {
+            if (step.isUnderflow()) {
+                if (endOfInput) {
+                    step = decoder.flush(decoded);
+                    break;
+                } if (wrapper.hasRemaining()) {
+                    final int unprocessed = wrapper.remaining();
+                    final byte[] next = contents.get(++arrayIndex);
+                    final ByteBuffer previous = wrapper;
+                    wrapper = ByteBuffer.allocate(unprocessed + next.length);
+                    wrapper.put(previous);
+                    wrapper.put(next);
+                    processed += wrapper.position() - unprocessed;
+                    wrapper.flip();
+                } else {
+                    final byte[] next = contents.get(++arrayIndex);
+                    final int wrapSize = Math.min(next.length, viewSpan - processed);
+                    wrapper = ByteBuffer.wrap(next, 0, wrapSize);
+                    processed += wrapSize;
+                }
+            } else if (step.isOverflow()) {
                 size = 2 * size + 1;
                 CharBuffer upsized = CharBuffer.allocate(size);
                 decoded.flip();
@@ -591,11 +606,6 @@
                 decoded = upsized;
                 continue;
             }
-
-            byte[] next = contents.get(++arrayIndex);
-            int wrapSize = Math.min(next.length, viewSpan - processed);
-            wrapper = ByteBuffer.wrap(next, 0, wrapSize);
-            processed += wrapSize;
         } while (!step.isError());
 
         if (step.isError()) {
diff --git a/proton-j/src/test/java/org/apache/qpid/proton/codec/CompositeReadableBufferTest.java b/proton-j/src/test/java/org/apache/qpid/proton/codec/CompositeReadableBufferTest.java
index 9dc5c1b..6ae4592 100644
--- a/proton-j/src/test/java/org/apache/qpid/proton/codec/CompositeReadableBufferTest.java
+++ b/proton-j/src/test/java/org/apache/qpid/proton/codec/CompositeReadableBufferTest.java
@@ -26,6 +26,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.IOException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.InvalidMarkException;
@@ -3346,6 +3347,76 @@
         assertEquals("T", buffer.readString(StandardCharsets.UTF_8.newDecoder()));
     }
 
+    @Test
+    public void testReadUnicodeStringAcrossArrayBoundries() throws IOException {
+        String expected = "\u1f4a9\\u1f4a9\\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[utf8.length - 1];
+
+        System.arraycopy(utf8, 1, slice2, 0, slice2.length);
+
+        CompositeReadableBuffer composite = new CompositeReadableBuffer();
+        composite.append(slice1);
+        composite.append(slice2);
+
+        String result = composite.readUTF8();
+
+        assertEquals("Failed to round trip String correctly: ", expected, result);
+    }
+
+    @Test
+    public void testReadUnicodeStringAcrossMultipleArrayBoundries() throws IOException {
+        String expected = "\u1f4a9\\u1f4a9\\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[] { utf8[1], utf8[2] };
+        byte[] slice3 = new byte[] { utf8[3], utf8[4] };
+        byte[] slice4 = new byte[utf8.length - 5];
+
+        System.arraycopy(utf8, 5, slice4, 0, slice4.length);
+
+        CompositeReadableBuffer composite = new CompositeReadableBuffer();
+        composite.append(slice1);
+        composite.append(slice2);
+        composite.append(slice3);
+        composite.append(slice4);
+
+        String result = composite.readUTF8();
+
+        assertEquals("Failed to round trip String correctly: ", expected, result);
+    }
+
+    @Test
+    public void testReadUnicodeStringEachByteInOwnArray() throws IOException {
+        String expected = "\u1f4a9";
+
+        byte[] utf8 = expected.getBytes(StandardCharsets.UTF_8);
+
+        assertEquals(4, utf8.length);
+
+        byte[] slice1 = new byte[] { utf8[0] };
+        byte[] slice2 = new byte[] { utf8[1] };
+        byte[] slice3 = new byte[] { utf8[2] };
+        byte[] slice4 = new byte[] { utf8[3] };
+
+        System.arraycopy(utf8, 1, slice2, 0, slice2.length);
+
+        CompositeReadableBuffer composite = new CompositeReadableBuffer();
+        composite.append(slice1);
+        composite.append(slice2);
+        composite.append(slice3);
+        composite.append(slice4);
+
+        String result = composite.readUTF8();
+
+        assertEquals("Failed to round trip String correctly: ", expected, result);
+    }
+
     //----- Tests for hashCode -----------------------------------------------//
 
     @Test
diff --git a/proton-j/src/test/java/org/apache/qpid/proton/codec/StringTypeTest.java b/proton-j/src/test/java/org/apache/qpid/proton/codec/StringTypeTest.java
index 3bf3985..a6e6685 100644
--- a/proton-j/src/test/java/org/apache/qpid/proton/codec/StringTypeTest.java
+++ b/proton-j/src/test/java/org/apache/qpid/proton/codec/StringTypeTest.java
@@ -454,4 +454,43 @@
             delegate.put(src);
         }
     }
+
+    @Test
+    public void testEncodeAndDecodeLargeUnicodeString() throws IOException {
+        StringBuilder unicodeStringBuilder = new StringBuilder();
+
+        unicodeStringBuilder.append((char) 1000);
+        unicodeStringBuilder.append((char) 1001);
+        unicodeStringBuilder.append((char) 1002);
+        unicodeStringBuilder.append((char) 1003);
+
+        final DecoderImpl decoder = new DecoderImpl();
+        final EncoderImpl encoder = new EncoderImpl(decoder);
+        AMQPDefinedTypes.registerAllTypes(decoder, encoder);
+        final ByteBuffer bb = ByteBuffer.allocate(1024);
+
+        final AmqpValue inputValue = new AmqpValue(unicodeStringBuilder.toString());
+        encoder.setByteBuffer(bb);
+        encoder.writeObject(inputValue);
+
+        final int size1 = bb.position() / 2;
+        final int size2 = bb.position() - size1;
+
+        final byte[] slice1 = new byte[size1];
+        final byte[] slice2 = new byte[size2];
+
+        bb.flip();
+        bb.get(slice1);
+        bb.get(slice2);
+
+        CompositeReadableBuffer composite = new CompositeReadableBuffer();
+        composite.append(slice1);
+        composite.append(slice2);
+
+        decoder.setBuffer(composite);
+
+        final AmqpValue outputValue = (AmqpValue) decoder.readObject();
+
+        assertEquals("Failed to round trip String correctly: ", unicodeStringBuilder.toString(), outputValue.getValue());
+    }
 }