PROTON-1836 Fix empty string decoding from CompositeReadableBuffer

Ensure that empty string is read as empty and not null string.  Adds
tests and optimizes the StringType encodings to fast return on size zero
strings.
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 5ab0348..5a030f0 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
@@ -531,7 +531,7 @@
     @Override
     public String readString(CharsetDecoder decoder) throws CharacterCodingException {
         if (!hasRemaining()) {
-            return null;
+            return "";
         }
 
         CharBuffer decoded = null;
diff --git a/proton-j/src/main/java/org/apache/qpid/proton/codec/StringType.java b/proton-j/src/main/java/org/apache/qpid/proton/codec/StringType.java
index dfc449c..24bea47 100644
--- a/proton-j/src/main/java/org/apache/qpid/proton/codec/StringType.java
+++ b/proton-j/src/main/java/org/apache/qpid/proton/codec/StringType.java
@@ -157,7 +157,7 @@
         {
             DecoderImpl decoder = getDecoder();
             int size = decoder.readRawInt();
-            return decoder.readRaw(_stringCreator, size);
+            return size == 0 ? "" : decoder.readRaw(_stringCreator, size);
         }
 
         public void setValue(final String val, final int length)
@@ -219,7 +219,7 @@
         {
             DecoderImpl decoder = getDecoder();
             int size = ((int)decoder.readRawByte()) & 0xff;
-            return decoder.readRaw(_stringCreator, size);
+            return size == 0 ? "" : decoder.readRaw(_stringCreator, size);
         }
 
         public void setValue(final String val, final int length)
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 45626bc..8c26555 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
@@ -2079,7 +2079,7 @@
     public void testReadStringFromEmptyBuffer() throws CharacterCodingException {
         CompositeReadableBuffer buffer = new CompositeReadableBuffer();
 
-        assertNull(buffer.readString(StandardCharsets.UTF_8.newDecoder()));
+        assertEquals("", buffer.readString(StandardCharsets.UTF_8.newDecoder()));
     }
 
     @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 6c376d5..320bd54 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
@@ -149,6 +149,69 @@
         assertEquals("read", result);
     }
 
+    @Test
+    public void testEncodeAndDecodeEmptyString() {
+        final DecoderImpl decoder = new DecoderImpl();
+        final EncoderImpl encoder = new EncoderImpl(decoder);
+        AMQPDefinedTypes.registerAllTypes(decoder, encoder);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(64);
+
+        encoder.setByteBuffer(buffer);
+        decoder.setByteBuffer(buffer);
+
+        encoder.writeString("a");
+        encoder.writeString("");
+        encoder.writeString("b");
+
+        buffer.clear();
+
+        TypeConstructor<?> stringType = decoder.readConstructor();
+        assertEquals(String.class, stringType.getTypeClass());
+        stringType.skipValue();
+
+        String result = decoder.readString();
+        assertEquals("", result);
+        result = decoder.readString();
+        assertEquals("b", result);
+    }
+
+    @Test
+    public void testEmptyShortStringEncod() {
+        doTestEmptyStringEncodAsGivenType(EncodingCodes.STR8);
+    }
+
+    @Test
+    public void testEmptyLargeStringEncod() {
+        doTestEmptyStringEncodAsGivenType(EncodingCodes.STR32);
+    }
+
+    public void doTestEmptyStringEncodAsGivenType(byte encodingCode) {
+        final DecoderImpl decoder = new DecoderImpl();
+        final EncoderImpl encoder = new EncoderImpl(decoder);
+        AMQPDefinedTypes.registerAllTypes(decoder, encoder);
+
+        final ByteBuffer buffer = ByteBuffer.allocate(64);
+
+        buffer.put(encodingCode);
+        buffer.putInt(0);
+        buffer.clear();
+
+        byte[] copy = new byte[buffer.remaining()];
+        buffer.get(copy);
+
+        CompositeReadableBuffer composite = new CompositeReadableBuffer();
+        composite.append(copy);
+
+        decoder.setBuffer(composite);
+
+        TypeConstructor<?> stringType = decoder.readConstructor();
+        assertEquals(String.class, stringType.getTypeClass());
+
+        String result = (String) stringType.readValue();
+        assertEquals("", result);
+    }
+
     // build up some test data with a set of suitable Unicode characters
     private static List<String> generateTestData()
     {