SLING-11468 : Simplify JcrPropertyMapCacheEntry.convertToType (#31)

* SLING-11468 : Simplify JcrPropertyMapCacheEntry.convertToType

* review findings by joerg hoh
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/JcrValueMap.java b/src/main/java/org/apache/sling/jcr/resource/internal/JcrValueMap.java
index f435770..8955971 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/JcrValueMap.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/JcrValueMap.java
@@ -251,11 +251,7 @@
             if (entry == null) {
                 entry = new JcrPropertyMapCacheEntry(prop);
                 cache.put(key, entry);
-
-                final Object defaultValue = entry.getPropertyValue();
-                if (defaultValue != null) {
-                    valueCache.put(key, entry.getPropertyValue());
-                }
+                valueCache.put(key, entry.getPropertyValue());
             }
             return entry;
         } catch (final RepositoryException re) {
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntry.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntry.java
index d5f1efd..33ee26d 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntry.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntry.java
@@ -284,79 +284,84 @@
         if (type.isInstance(initialValue)) {
             return (T) initialValue;
         }
+        
+        if (initialValue instanceof InputStream) {
+            return convertInputStream(index, (InputStream) initialValue, type, node, dynamicClassLoader);
+        } else {
+            return convert(initialValue, type, node);
+        }
+    }
+    
+    private @Nullable <T> T convertInputStream(int index,
+                                               final @NotNull InputStream value,
+                                               final @NotNull Class<T> type,
+                                               final @NotNull Node node,
+                                               final @Nullable ClassLoader dynamicClassLoader) throws RepositoryException {
+        // object input stream
+        if (ObjectInputStream.class.isAssignableFrom(type)) {
+            try {
+                return (T) new PropertyObjectInputStream(value, dynamicClassLoader);
+            } catch (final IOException ioe) {
+                // ignore and use fallback
+            }
 
-        Object value = initialValue;
-
-        // special case input stream first
-        if (value instanceof InputStream) {
-            // object input stream
-            if (ObjectInputStream.class.isAssignableFrom(type)) {
-                try {
-                    return (T) new PropertyObjectInputStream((InputStream) value, dynamicClassLoader);
-                } catch (final IOException ioe) {
-                    // ignore and use fallback
+        // any number: length of binary
+        } else if (Number.class.isAssignableFrom(type)) {
+            // avoid NPE if this instance has not been created from a property (see SLING-11465)
+            if (property == null) {
+                return null;
+            }
+            return convert(propertyToLength(property, index), type, node);
+            
+        // string: read binary
+        } else if (String.class == type) {
+            return (T) inputStreamToString(value);
+            
+        // any serializable
+        } else if (Serializable.class.isAssignableFrom(type)) {
+            try (ObjectInputStream ois = new PropertyObjectInputStream(value, dynamicClassLoader)) {
+                final Object obj = ois.readObject();
+                if (type.isInstance(obj)) {
+                    return (T) obj;
                 }
-
-                // any number: length of binary
-            } else if (Number.class.isAssignableFrom(type)) {
-                // avoid NPE if this instance has not been created from a property (see SLING-11465)
-                if (property == null) {
-                    return null;
-                } 
-                
-                if (index == -1) {
-                    value = Long.valueOf(this.property.getLength());
-                } else {
-                    value = Long.valueOf(this.property.getLengths()[index]);
-                }
-
-                // string: read binary
-            } else if (String.class == type) {
-                final InputStream in = (InputStream) value;
-                try {
-                    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                    final byte[] buffer = new byte[2048];
-                    int l;
-                    while ((l = in.read(buffer)) >= 0) {
-                        if (l > 0) {
-                            baos.write(buffer, 0, l);
-                        }
-                    }
-                    value = new String(baos.toByteArray(), StandardCharsets.UTF_8);
-                } catch (final IOException e) {
-                    throw new IllegalArgumentException(e);
-                } finally {
-                    try {
-                        in.close();
-                    } catch (final IOException ignore) {
-                        // ignore
-                    }
-                }
-
-                // any serializable
-            } else if (Serializable.class.isAssignableFrom(type)) {
-                ObjectInputStream ois = null;
-                try {
-                    ois = new PropertyObjectInputStream((InputStream) value, dynamicClassLoader);
-                    final Object obj = ois.readObject();
-                    if (type.isInstance(obj)) {
-                        return (T) obj;
-                    }
-                    value = obj;
-                } catch (final ClassNotFoundException | IOException cnfe) {
-                    // ignore and use fallback
-                } finally {
-                    if (ois != null) {
-                        try {
-                            ois.close();
-                        } catch (final IOException ignore) {
-                            // ignore
-                        }
-                    }
+                return convert(obj, type, node);
+            } catch (final ClassNotFoundException | IOException cnfe) {
+                // ignore and use fallback
+            }
+            // ignore
+        } 
+        
+        // fallback
+        return convert(value, type, node);
+    }
+    
+    private static @NotNull Long propertyToLength(@NotNull Property property, int index) throws RepositoryException {
+        if (index == -1) {
+            return Long.valueOf(property.getLength());
+        } else {
+            return Long.valueOf(property.getLengths()[index]);
+        }
+    }
+    
+    private static @NotNull String inputStreamToString(@NotNull InputStream value) {
+        try (InputStream in = value) {
+            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            final byte[] buffer = new byte[2048];
+            int l;
+            while ((l = in.read(buffer)) >= 0) {
+                if (l > 0) {
+                    baos.write(buffer, 0, l);
                 }
             }
+            return new String(baos.toByteArray(), StandardCharsets.UTF_8);
+        } catch (final IOException e) {
+            throw new IllegalArgumentException(e);
         }
+    }
 
+    private @Nullable <T> T convert(final @NotNull Object value,
+                                   final @NotNull Class<T> type,
+                                   final @NotNull Node node) throws RepositoryException {
         if (String.class == type) {
             return (T) getConverter(value).toString();
 
diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntryTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntryTest.java
index d53c08d..72fd25a 100644
--- a/src/test/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntryTest.java
+++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/JcrPropertyMapCacheEntryTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.jcr.resource.internal.helper;
 
+import com.google.common.collect.Maps;
 import org.apache.jackrabbit.value.BooleanValue;
 import org.apache.jackrabbit.value.ValueFactoryImpl;
 import org.junit.Before;
@@ -26,21 +27,27 @@
 import javax.jcr.Node;
 import javax.jcr.Property;
 import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
 import javax.jcr.Session;
+import javax.jcr.Value;
 import javax.jcr.ValueFactory;
 import javax.jcr.ValueFormatException;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.Calendar;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -119,6 +126,33 @@
         verifyZeroInteractions(node);
     }
     
+    @Test(expected = IllegalArgumentException.class)
+    public void testCannotStore() throws Exception {
+        Object value = new TestClass();
+        new JcrPropertyMapCacheEntry(value, node);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCannotStoreArray() throws Exception {
+        Object value = new TestClass();
+        new JcrPropertyMapCacheEntry(new Object[] {value}, node);
+    }
+    
+    @Test
+    public void testGetPropertyValueOrNull() throws Exception {
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(true, node);
+        assertEquals(Boolean.TRUE, entry.getPropertyValueOrNull());
+    }
+    
+    @Test
+    public void testGetPropertyValueOrNullWithRepositoryException() throws Exception {
+        Property prop = mock(Property.class);
+        when(prop.getType()).thenReturn(PropertyType.BINARY);
+        when(prop.getValue()).thenThrow(new RepositoryException());
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
+        assertNull(entry.getPropertyValueOrNull());
+    }
+    
     @Test
     public void testInputStreamToString() throws Exception {
         InputStream in = new ByteArrayInputStream("test".getBytes());
@@ -151,7 +185,7 @@
     }
     
     @Test
-    public void testBinaryPropertyToLong() throws Exception {
+    public void testBinaryPropertyToInteger() throws Exception {
         Property prop = mock(Property.class);
         when(prop.getType()).thenReturn(PropertyType.BINARY);
         when(prop.getStream()).thenReturn(new ByteArrayInputStream("10".getBytes()));
@@ -159,8 +193,8 @@
         when(prop.getLength()).thenReturn(2L);
         
         JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
-        Long result = entry.convertToType(Long.class, node, null);
-        assertEquals(Long.valueOf(2), result);
+        Integer result = entry.convertToType(Integer.class, node, null);
+        assertEquals(Integer.valueOf(2), result);
         
         verify(prop, times(2)).isMultiple();
         verify(prop).getValue();
@@ -193,6 +227,31 @@
         verifyNoMoreInteractions(prop);
         verifyZeroInteractions(node);
     }
+
+    @Test
+    public void testMvBinaryPropertyToFloatArray() throws Exception {
+        Property prop = mock(Property.class);
+        when(prop.getType()).thenReturn(PropertyType.BINARY);
+        when(prop.isMultiple()).thenReturn(true);
+        when(prop.getValue()).thenThrow(new ValueFormatException("multi-valued"));
+        Value[] vs = new Value[] {vf.createValue("10.7", PropertyType.BINARY), vf.createValue("10.7", PropertyType.BINARY)};
+        when(prop.getValues()).thenReturn(vs);
+        when(prop.getLength()).thenThrow(new ValueFormatException("multi-valued"));
+        when(prop.getLengths()).thenReturn(new long[] {4L, 4L});
+
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
+        Float[] result = entry.convertToType(Float[].class, node, null);
+        assertNotNull(result);
+        assertEquals(2, result.length);
+        assertEquals(Float.valueOf(4.0f), result[0]);
+
+        verify(prop, times(2)).isMultiple();
+        verify(prop).getValues();
+        verify(prop).getType();
+        verify(prop, times(2)).getLengths();
+        verifyNoMoreInteractions(prop);
+        verifyZeroInteractions(node);
+    }
     
     @Test
     public void testInputStreamToObjectInputStream() throws Exception {
@@ -203,11 +262,38 @@
         assertNull(result); // TODO: is this the expected result?
         verifyZeroInteractions(node);
     }
+
+    @Test
+    public void testInputStreamToSerializableSameType() throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+            oos.writeObject(Maps.newHashMap());
+        }
+        
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(new ByteArrayInputStream(out.toByteArray()), node);
+        // same type
+        Map<?,?> result = entry.convertToType(HashMap.class, node, null);
+        assertNotNull(result);
+        verifyZeroInteractions(node);
+    }
+
+    @Test
+    public void testInputStreamToSerializable() throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+            oos.writeObject(new LinkedHashMap<>());
+        }
+
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(new ByteArrayInputStream(out.toByteArray()), node);
+        // different type that cannot be converted
+        Calendar result = entry.convertToType(Calendar.class, node, LinkedHashMap.class.getClassLoader());
+        assertNull(result);
+
+        verifyZeroInteractions(node);
+    }
     
     @Test
     public void testBinaryPropertyToObjectInputStream() throws Exception {
-        InputStream in = new ByteArrayInputStream("value".getBytes());
-
         Property prop = mock(Property.class);
         when(prop.getType()).thenReturn(PropertyType.BINARY);
         when(prop.getStream()).thenReturn(new ByteArrayInputStream("value".getBytes()));
@@ -225,11 +311,99 @@
     }
     
     @Test
+    public void testMvPropertyToBoolean() throws Exception {
+        Property prop = mock(Property.class);
+        when(prop.getType()).thenReturn(PropertyType.STRING);
+        when(prop.isMultiple()).thenReturn(true);
+        when(prop.getValue()).thenThrow(new ValueFormatException("multi-valued"));
+        Value[] vs = new Value[] {vf.createValue("true"), vf.createValue("false")};
+        when(prop.getValues()).thenReturn(vs);
+
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
+        assertTrue(entry.isArray());
+        
+        Boolean result = entry.convertToType(Boolean.class, node, null);
+        assertNotNull(result);
+        assertTrue(result);
+
+        verify(prop, times(2)).isMultiple();
+        verify(prop).getValues();
+        verify(prop).getType();
+        verifyNoMoreInteractions(prop);
+        verifyZeroInteractions(node);
+    }
+
+    @Test
+    public void testEmptyMvPropertyToString() throws Exception {
+        Property prop = mock(Property.class);
+        when(prop.getType()).thenReturn(PropertyType.STRING);
+        when(prop.isMultiple()).thenReturn(true);
+        when(prop.getValue()).thenThrow(new ValueFormatException("multi-valued"));
+        Value[] vs = new Value[0];
+        when(prop.getValues()).thenReturn(vs);
+
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
+        assertTrue(entry.isArray());
+        
+        String result = entry.convertToType(String.class, node, null);
+        assertNull(result);
+
+        verify(prop, times(2)).isMultiple();
+        verify(prop).getValues();
+        verify(prop).getType();
+        verifyNoMoreInteractions(prop);
+        verifyZeroInteractions(node);
+    }
+    
+    @Test
+    public void testConversionFails() throws RepositoryException {
+        Property prop = mock(Property.class);
+        when(prop.getType()).thenReturn(PropertyType.STRING);
+        when(prop.isMultiple()).thenReturn(false);
+        when(prop.getValue()).thenReturn(vf.createValue("string"));
+
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
+        assertFalse(entry.isArray());
+
+        Short result = entry.convertToType(Short.class, node, null);
+        assertNull(result);
+
+        verify(prop, times(2)).isMultiple();
+        verify(prop).getValue();
+        verify(prop).getType();
+        verifyNoMoreInteractions(prop);
+        verifyZeroInteractions(node);
+    }
+
+    @Test
+    public void testStringToValue() throws RepositoryException {
+        Property prop = mock(Property.class);
+        when(prop.getType()).thenReturn(PropertyType.STRING);
+        when(prop.isMultiple()).thenReturn(false);
+        when(prop.getValue()).thenReturn(vf.createValue("string"));
+
+        JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(prop);
+        assertFalse(entry.isArray());
+
+        Value result = entry.convertToType(Value.class, node, null);
+        assertNotNull(result);
+        assertEquals(PropertyType.STRING, result.getType());
+        assertEquals("string", result.getString());
+
+        verify(prop, times(2)).isMultiple();
+        verify(prop).getValue();
+        verify(prop).getType();
+        verify(node).getSession();
+        verifyNoMoreInteractions(prop, node);
+    }
+    
+    @Test
     public void testConvertToSameType() throws Exception {
         Calendar cal = Calendar.getInstance();
         
         JcrPropertyMapCacheEntry entry = new JcrPropertyMapCacheEntry(cal, node);
         Calendar result = entry.convertToType(Calendar.class, node, null);
+        
         assertSame(cal, result);
         verify(node).getSession();
         verifyNoMoreInteractions(node);
@@ -271,16 +445,11 @@
         assertTrue(propValue instanceof HashMap);
     }
     
-    @Test
+    @Test(expected = IllegalArgumentException.class)
     public void testCreateFromUnstorableValue() throws Exception {
-        try {
-            Object value = new TestClass();
-            new JcrPropertyMapCacheEntry(value, node);
-            fail("IllegalArgumentException expected");
-        } catch (IllegalArgumentException e) {
-            // success
-        }
+        Object value = new TestClass();
+        new JcrPropertyMapCacheEntry(value, node);
     }
     
-    private static final class TestClass {};
+    private static final class TestClass {}
 }
\ No newline at end of file