SLING-8935 support primitive arrays and collection types
diff --git a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
index ae637eb..2e57abe 100644
--- a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
+++ b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
@@ -19,46 +19,43 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.lang.reflect.Array;
+import java.util.Collection;
 import java.util.Dictionary;
 import java.util.Enumeration;
+import java.util.Iterator;
 
 import javax.json.stream.JsonGenerator;
 
 import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Configurations;
 
-
-/**
- * JSON writer for configurations
- */
+/** JSON writer for configurations */
 public class ConfigurationJSONWriter extends JSONWriterBase {
 
-    /**
-     * Writes the configurations to the writer.
-     * The writer is not closed.
+    /** Writes the configurations to the writer. The writer is not closed.
+     * 
      * @param writer Writer
      * @param configs List of configurations
-     * @throws IOException If writing fails
-     */
+     * @throws IOException If writing fails */
     public static void write(final Writer writer, final Configurations configs)
-    throws IOException {
+            throws IOException {
         final ConfigurationJSONWriter w = new ConfigurationJSONWriter();
         w.writeConfigurations(writer, configs);
     }
 
     private void writeConfigurations(final Writer writer, final Configurations configs)
-    throws IOException {
+            throws IOException {
         JsonGenerator generator = newGenerator(writer);
         writeConfigurations(generator, configs);
         generator.close();
     }
 
-    /**
-     * Write the OSGi configuration to a JSON structure as defined in <a href="https://osgi.org/specification/osgi.cmpn/7.0.0/service.configurator.html#d0e131765">OSGi Configurator Specification 1.0</a>.
-     * The writer is not closed. 
+    /** Write the OSGi configuration to a JSON structure as defined in
+     * <a href="https://osgi.org/specification/osgi.cmpn/7.0.0/service.configurator.html#d0e131765">OSGi Configurator Specification 1.0</a>.
+     * The writer is not closed.
+     * 
      * @param writer Writer
-     * @param props The configuration properties to write
-     */
+     * @param props The configuration properties to write */
     public static void writeConfiguration(final Writer writer, final Dictionary<String, Object> props) {
         final ConfigurationJSONWriter w = new ConfigurationJSONWriter();
         JsonGenerator generator = w.newGenerator(writer);
@@ -70,9 +67,9 @@
 
     protected static void writeConfiguration(final JsonGenerator generator, final Dictionary<String, Object> props) {
         final Enumeration<String> e = props.keys();
-        while ( e.hasMoreElements() ) {
+        while (e.hasMoreElements()) {
             final String name = e.nextElement();
-            if ( Configuration.PROP_ARTIFACT_ID.equals(name) ) {
+            if (Configuration.PROP_ARTIFACT_ID.equals(name)) {
                 continue;
             }
             final Object val = props.get(name);
@@ -80,63 +77,103 @@
         }
     }
 
-    protected static void writeConfigurationProperty(JsonGenerator generator, String name, Object val) {
-        String typePostFix = null;
-        final Object typeCheck;
-        if ( val.getClass().isArray() ) {
-            if ( Array.getLength(val) > 0 ) {
-                typeCheck = Array.get(val, 0);
-            } else {
-                typeCheck = null;
-            }
-        } else {
-            typeCheck = val;
+    private static void writeConfigurationProperty(JsonGenerator generator, String name, Object val) {
+        String dataType = getDataType(val);
+        writeConfigurationProperty(generator, name, dataType, val);
+    }
+
+    private static void writeConfigurationProperty(JsonGenerator generator, String name, String dataType, Object val) {
+        String nameWithDataPostFix = name;
+        if (dataType != null) {
+            nameWithDataPostFix += ":" + dataType;
         }
-
-        if ( typeCheck instanceof Integer ) {
-            typePostFix = ":Integer";
-        } else if ( typeCheck instanceof Byte ) {
-            typePostFix = ":Byte";
-        } else if ( typeCheck instanceof Character ) {
-            typePostFix = ":Character";
-        } else if ( typeCheck instanceof Float ) {
-            typePostFix = ":Float";
-        }
-
-        if ( val.getClass().isArray() ) {
-            generator.writeStartArray(name);
-            for(int i=0; i<Array.getLength(val);i++ ) {
-                final Object obj = Array.get(val, i);
-                if ( typePostFix == null ) {
-                    if ( obj instanceof String ) {
-                        generator.write((String)obj);
-                    } else if ( obj instanceof Boolean ) {
-                        generator.write((Boolean)obj);
-                    } else if ( obj instanceof Long ) {
-                        generator.write((Long)obj);
-                    } else if ( obj instanceof Double ) {
-                        generator.write((Double)obj);
-                    }
-                } else {
-                    generator.write(obj.toString());
-                }
+        if (val.getClass().isArray()) {
+            generator.writeStartArray(nameWithDataPostFix);
+            for (int i = 0; i < Array.getLength(val); i++) {
+                writeArrayItem(generator, Array.get(val, i));
             }
-
+            generator.writeEnd();
+        } else if (val instanceof Collection) {
+            generator.writeStartArray(nameWithDataPostFix);
+            for (Object item : Collection.class.cast(val)) {
+                writeArrayItem(generator, item);
+            }
             generator.writeEnd();
         } else {
-            if ( typePostFix == null ) {
-                if ( val instanceof String ) {
-                    generator.write(name, (String)val);
-                } else if ( val instanceof Boolean ) {
-                    generator.write(name, (Boolean)val);
-                } else if ( val instanceof Long ) {
-                    generator.write(name, (Long)val);
-                } else if ( val instanceof Double ) {
-                    generator.write(name, (Double)val);
-                }
+            writeNameValuePair(generator, nameWithDataPostFix, val);
+        }
+    }
+
+    private static void writeNameValuePair(JsonGenerator generator, String name, Object item) {
+        if (item instanceof Boolean) {
+            generator.write(name, (Boolean) item);
+        } else if (item instanceof Long || item instanceof Integer || item instanceof Byte || item instanceof Short) {
+            generator.write(name, ((Number)item).longValue());
+        } else if (item instanceof Double) {
+            generator.write(name, (Double) item);
+        } else if (item instanceof Float) {
+            generator.write(name, (Float) item);
+        } else {
+            generator.write(name, item.toString());
+        }
+    }
+
+    private static void writeArrayItem(JsonGenerator generator, Object item) {
+        if (item instanceof Boolean) {
+            generator.write((Boolean) item);
+        } else if (item instanceof Long || item instanceof Integer || item instanceof Byte || item instanceof Short) {
+            generator.write(((Number)item).longValue());
+        } else if (item instanceof Double) {
+            generator.write((Double) item);
+        } else if (item instanceof Float) {
+            generator.write((Float) item);
+        } else {
+            generator.write(item.toString());
+        }
+    }
+
+    private static String getDataType(Object object) {
+        if (object instanceof Collection) {
+            // check class of first item
+            Iterator<?> it = ((Collection<?>) object).iterator();
+            if (it.hasNext()) {
+                Class<?> itemClass = it.next().getClass();
+                return "Collection<" + getDataType(itemClass, false) + ">";
             } else {
-                generator.write(name + typePostFix, val.toString());
+                throw new IllegalStateException("Empty collections are invalid");
+            }
+        } else {
+            return getDataType(object.getClass(), true);
+        }
+    }
+
+    private static String getDataType(Class<?> clazz, boolean allowEmpty) {
+        if (clazz.isArray()) {
+            String dataType = getDataType(clazz.getComponentType(), false);
+            if (dataType != null) {
+                return dataType + "[]";
+            } else {
+                return null;
             }
         }
+        // default classes used by native JSON types
+        else if (clazz.isAssignableFrom(Boolean.class) || clazz.isAssignableFrom(boolean.class) || clazz.isAssignableFrom(Long.class) || clazz.isAssignableFrom(long.class) ||
+                clazz.isAssignableFrom(Double.class) || clazz.isAssignableFrom(double.class) || clazz.isAssignableFrom(String.class)) {
+            // no data type necessary except when being used in an array/collection
+            if (!allowEmpty) {
+                // for all other cases just use the simple name
+                return clazz.getSimpleName();
+            }
+
+        } else if (clazz.isAssignableFrom(Integer.class) || clazz.isAssignableFrom(int.class) || clazz.isAssignableFrom(Float.class) || clazz.isAssignableFrom(float.class)
+                || clazz.isAssignableFrom(Byte.class) || clazz.isAssignableFrom(byte.class) || clazz.isAssignableFrom(Short.class) || clazz.isAssignableFrom(short.class)
+                || clazz.isAssignableFrom(Character.class) || clazz.isAssignableFrom(char.class)) {
+            return clazz.getSimpleName();
+        }
+        if (!allowEmpty) {
+            throw new IllegalStateException("Class does not have a valid type " + clazz);
+        }
+        return null;
+
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/io/package-info.java b/src/main/java/org/apache/sling/feature/io/package-info.java
index 25e2f46..81fdbf9 100644
--- a/src/main/java/org/apache/sling/feature/io/package-info.java
+++ b/src/main/java/org/apache/sling/feature/io/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@org.osgi.annotation.versioning.Version("1.0.0")
+@org.osgi.annotation.versioning.Version("1.1.0")
 package org.apache.sling.feature.io;
 
 
diff --git a/src/test/java/org/apache/sling/feature/io/json/ConfigurationJSONWriterTest.java b/src/test/java/org/apache/sling/feature/io/json/ConfigurationJSONWriterTest.java
index d40cfa8..13a7b37 100644
--- a/src/test/java/org/apache/sling/feature/io/json/ConfigurationJSONWriterTest.java
+++ b/src/test/java/org/apache/sling/feature/io/json/ConfigurationJSONWriterTest.java
@@ -26,6 +26,8 @@
 import java.util.Hashtable;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -33,9 +35,14 @@
 import org.apache.felix.configurator.impl.json.TypeConverter;
 import org.apache.felix.configurator.impl.model.ConfigurationFile;
 import org.hamcrest.Description;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
 import org.hamcrest.TypeSafeMatcher;
+import org.hamcrest.core.Every;
 import org.junit.Assert;
 import org.junit.Test;
+import org.osgi.util.converter.Converter;
+import org.osgi.util.converter.Converters;
+import org.osgi.util.converter.TypeReference;
 
 public class ConfigurationJSONWriterTest {
 
@@ -90,15 +97,56 @@
         sb.append("\" : ");
         sb.append(json);
         sb.append("}");
-        final ConfigurationFile configurationFile = JSONUtil.readJSON(new TypeConverter(null),"name", new URL("http://127.0.0.1/configtest"),
+        final ConfigurationFile configurationFile = JSONUtil.readJSON(new TypeConverter(null),"name", new URL("file:///configtest"),
                 0, sb.toString(), report);
         if ( !report.errors.isEmpty() || !report.warnings.isEmpty() ) {
             Assert.fail("JSON is not the right format: \nErrors: " + StringUtils.join(report.errors) + "\nWarnings: " + StringUtils.join(report.warnings));
         }
-        Assert.assertThat(configurationFile.getConfigurations().get(0).getProperties(), new DictionaryMatcher(expectedProps));
+        // convert to maps for easier comparison
+        Converter converter = Converters.standardConverter();
+        Map<String, Object> expectedPropsMap = converter.convert(expectedProps).to(new TypeReference<Map<String,Object>>(){});
+        Map<String, Object> actualPropsMap = converter.convert(configurationFile.getConfigurations().get(0).getProperties()).to(new TypeReference<Map<String,Object>>(){});
+        Assert.assertThat(actualPropsMap.entrySet(), Every.everyItem(new MapEntryMatcher<>(expectedPropsMap)));
+    }
+    
+    public static class MapEntryMatcher<K, V> extends TypeSafeDiagnosingMatcher<Map.Entry<K, V>> {
+
+        private final Map<K,V> expectedMap;
+
+        public MapEntryMatcher(Map<K, V> expectedMap) {
+            this.expectedMap = expectedMap;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("contained in the expected map");
+        }
+
+        
+        @Override
+        protected boolean matchesSafely(Map.Entry<K, V> item, Description description) {
+            if (expectedMap.get(item.getKey()) == null){
+                description.appendText("key '" + item.getKey() + "' is not present");
+                return false;
+            } else {
+                boolean isEqual;
+                if (item.getValue().getClass().isArray()) {
+                    isEqual = Objects.deepEquals(expectedMap.get(item.getKey()), item.getValue());
+                    
+                } else {
+                    isEqual = expectedMap.get(item.getKey()).equals(item.getValue());
+                }
+                if (!isEqual) {
+                    description.appendText("has the wrong value for key '" + item.getKey() + "': Expected=" + expectedMap.get(item.getKey()) + " (" + expectedMap.get(item.getKey()).getClass() + ")" + ", Actual=" + item.getValue() + " (" + item.getValue().getClass() + ")");
+                }
+                return isEqual;
+            }
+        }
     }
     
     public final static class DictionaryMatcher extends TypeSafeMatcher<Dictionary<String, Object>> {
+        
+        // internally use maps
         private final Dictionary<String, Object> expectedDictionary;
 
         public DictionaryMatcher(Dictionary<String, Object> dictionary) {
@@ -139,6 +187,8 @@
 
         @Override
         protected boolean matchesSafely(Dictionary<String, Object> item) {
+            // use a map for comparison
+            //expectedDictionary.x
             return expectedDictionary.equals(item);
         }