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);
}