Merge pull request #14 from apache/feature/SLING-8419
SLING-8419 refactor method to serialize OSGi configs as JSON
diff --git a/pom.xml b/pom.xml
index 0af896f..4c0453e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,29 +1,30 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
- <!--
- Licensed to the Apache Software Foundation (ASF) under one or more contributor license
- agreements. See the NOTICE file distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file to you under the Apache License,
- Version 2.0 (the "License"); you may not use this file except in compliance with the
- License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ agreements. See the NOTICE file distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file to you under the Apache License,
+ Version 2.0 (the "License"); you may not use this file except in compliance with the
+ License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software distributed under the
- License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
- either express or implied. See the License for the specific language governing permissions
- and limitations under the License.
- -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ Unless required by applicable law or agreed to in writing, software distributed under the
+ License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ either express or implied. See the License for the specific language governing permissions
+ and limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.sling</groupId>
<artifactId>sling</artifactId>
<version>34</version>
- <relativePath />
+ <relativePath/>
</parent>
<artifactId>org.apache.sling.feature.io</artifactId>
<version>1.1.1-SNAPSHOT</version>
<packaging>bundle</packaging>
-
+
<name>Apache Sling Feature IO Module</name>
<description>
IO functionality for the Feature Model
@@ -37,8 +38,8 @@
<connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-io.git</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-io.git</developerConnection>
<url>https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-feature-io.git</url>
- <tag>HEAD</tag>
- </scm>
+ <tag>HEAD</tag>
+ </scm>
<build>
<plugins>
@@ -184,5 +185,11 @@
<version>1.0.0</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.9</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/feature/io/CloseShieldWriter.java b/src/main/java/org/apache/sling/feature/io/CloseShieldWriter.java
new file mode 100644
index 0000000..368fd28
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/io/CloseShieldWriter.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.sling.feature.io;
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Proxy writer that prevents the underlying writer from being closed.
+ * <p>
+ * This class is typically used in cases where a writer needs to be passed to a component that wants to explicitly close
+ * the writer even if other components would still use the writer for output.
+ * </p>
+ */
+public class CloseShieldWriter extends FilterWriter {
+
+ public CloseShieldWriter(Writer out) {
+ super(out);
+ }
+
+ @Override
+ public void close() throws IOException {
+ // NOOP
+ }
+
+}
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 3732a5e..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
@@ -18,39 +18,162 @@
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);
-
- // TODO is this correct?
- generator.writeStartObject(JSONConstants.FEATURE_CONFIGURATIONS);
writeConfigurations(generator, configs);
- generator.writeEnd();
-
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.
+ *
+ * @param writer Writer
+ * @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);
+ generator.writeStartObject();
+ writeConfiguration(generator, props);
+ generator.writeEnd();
+ generator.close();
+ }
+
+ protected static void writeConfiguration(final JsonGenerator generator, final Dictionary<String, Object> props) {
+ final Enumeration<String> e = props.keys();
+ while (e.hasMoreElements()) {
+ final String name = e.nextElement();
+ if (Configuration.PROP_ARTIFACT_ID.equals(name)) {
+ continue;
+ }
+ final Object val = props.get(name);
+ writeConfigurationProperty(generator, name, 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 (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 {
+ 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 {
+ 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/json/FeatureJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
index 2c7af7c..af36576 100644
--- a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
@@ -47,6 +47,13 @@
// protected constructor for subclassing
}
+ /**
+ * Writes the feature to the writer.
+ * The writer is not closed.
+ * @param writer Writer
+ * @param feature Feature
+ * @throws IOException If writing fails
+ */
protected void writeFeature(final Writer writer, final Feature feature)
throws IOException {
JsonGenerator generator = newGenerator(writer);
diff --git a/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java b/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
index 66b6ed2..d88f288 100644
--- a/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
+++ b/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
@@ -17,10 +17,8 @@
package org.apache.sling.feature.io.json;
import java.io.Writer;
-import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.List;
import java.util.Map;
@@ -37,6 +35,7 @@
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.MatchingRequirement;
import org.apache.sling.feature.Prototype;
+import org.apache.sling.feature.io.CloseShieldWriter;
import org.osgi.resource.Capability;
import org.osgi.resource.Requirement;
@@ -48,7 +47,9 @@
private final JsonGeneratorFactory generatorFactory = Json.createGeneratorFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true));
protected final JsonGenerator newGenerator(final Writer writer) {
- return generatorFactory.createGenerator(writer);
+ // prevent closing of the underlying writer
+ Writer closeShieldWriter = new CloseShieldWriter(writer);
+ return generatorFactory.createGenerator(closeShieldWriter);
}
protected void writeBundles(final JsonGenerator generator,
@@ -104,75 +105,7 @@
for(final Configuration cfg : cfgs) {
generator.writeStartObject(cfg.getPid());
-
- final Enumeration<String> e = cfg.getProperties().keys();
- while ( e.hasMoreElements() ) {
- final String name = e.nextElement();
- if ( Configuration.PROP_ARTIFACT_ID.equals(name) ) {
- continue;
- }
-
- final Object val = cfg.getProperties().get(name);
-
- 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;
- }
-
- 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());
- }
- }
-
- 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);
- }
- } else {
- generator.write(name + typePostFix, val.toString());
- }
- }
- }
-
+ ConfigurationJSONWriter.writeConfiguration(generator, cfg.getConfigurationProperties());
generator.writeEnd();
}
diff --git a/src/main/java/org/apache/sling/feature/io/json/package-info.java b/src/main/java/org/apache/sling/feature/io/json/package-info.java
index 6b84931..54ecf2e 100644
--- a/src/main/java/org/apache/sling/feature/io/json/package-info.java
+++ b/src/main/java/org/apache/sling/feature/io/json/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.json;
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
new file mode 100644
index 0000000..13a7b37
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/io/json/ConfigurationJSONWriterTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.feature.io.json;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+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;
+import org.apache.felix.configurator.impl.json.JSONUtil;
+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 {
+
+ @Test
+ public void testConfigurationWriteReadRoundtrip() throws IOException {
+ Dictionary<String, Object> props = new Hashtable<>();
+ props.put("Integer-simple", 1);
+ props.put("Integer-array", new Integer[]{1,2});
+ props.put("Integer-list", Arrays.asList(1, 2));
+ props.put("int-array", new int[]{1,2});
+ props.put("Long-simple", 1);
+ props.put("Long-array", new Long[]{1L,2L});
+ props.put("Long-list", Arrays.asList(1l, 2l));
+ props.put("long-array", new long[]{1,2});
+ props.put("Boolean-simple", Boolean.TRUE);
+ props.put("Boolean-array", new Boolean[] {Boolean.TRUE, Boolean.FALSE});
+ props.put("Boolean-list", Arrays.asList(Boolean.TRUE, Boolean.FALSE));
+ props.put("bool-array", new boolean[] {true, false});
+ props.put("Float-simple", 1.0d);
+ props.put("Float-array", new Float[]{1.0f, 2.0f});
+ props.put("Float-list", Arrays.asList(1.0f, 2.0f));
+ props.put("float-array", new float[]{1.0f,2.0f});
+ props.put("Double-simple", 1.0d);
+ props.put("Double-array", new Double[]{1.0d,2.0d});
+ props.put("Double-list", Arrays.asList(1.0d, 2.0d));
+ props.put("double-array", new double[]{1.0d,2.0d});
+ props.put("Byte-simple", new Byte((byte)1));
+ props.put("Byte-array", new Byte[]{1,2});
+ props.put("Byte-list", Arrays.asList((byte)1, (byte)2));
+ props.put("byte-array", new byte[]{1,2});
+ props.put("Short-simple", new Short((short) 1));
+ props.put("Short-array", new Short[]{1,2});
+ props.put("Short-list", Arrays.asList((short)1, (short)2));
+ props.put("Short-array", new short[]{1,2});
+ props.put("Character-simple", 1);
+ props.put("Character-array", new Character[]{'a','b'});
+ props.put("Character-list", Arrays.asList('a', 'b'));
+ props.put("char-array", new char[]{'a','b'});
+ props.put("String-simple", "test");
+ props.put("String-array", new String[]{"test1", "test2"});
+ props.put("String-list", Arrays.asList("test1", "test2"));
+ StringWriter writer = new StringWriter();
+ ConfigurationJSONWriter.writeConfiguration(writer, props);
+ writer.close();
+ assertConfigurationJson(writer.toString(), props);
+ }
+
+ protected void assertConfigurationJson(String json, Dictionary<String, Object> expectedProps) throws MalformedURLException {
+ final JSONUtil.Report report = new JSONUtil.Report();
+ StringBuilder sb = new StringBuilder("{ \"");
+ sb.append("myid");
+ sb.append("\" : ");
+ sb.append(json);
+ sb.append("}");
+ 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));
+ }
+ // 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) {
+ this.expectedDictionary = dictionary;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Dictionary with items: ").appendValueList("", ",", "", toString(expectedDictionary));
+ }
+
+
+ @Override
+ protected void describeMismatchSafely(Dictionary<String, Object> item, Description mismatchDescription) {
+ mismatchDescription.appendText("was Dictionary with items: ").appendValueList("", ",", "", toString(item));
+ }
+
+ static Iterable<String> toString(Dictionary<String, Object> dictionary) {
+ List<String> itemList = new LinkedList<String>();
+ Enumeration<String> e = dictionary.keys();
+ while (e.hasMoreElements()) {
+ final String key = e.nextElement();
+ Object value = dictionary.get(key);
+ // for arrays expand values
+ final String type = value.getClass().getSimpleName();
+ if (value.getClass().isArray()) {
+ value = ArrayUtils.toString(value);
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append(key).append(":");
+ sb.append(value);
+ sb.append("(").append(type).append(")");
+ sb.append(", ");
+ itemList.add(sb.toString());
+ }
+ return itemList;
+ }
+
+ @Override
+ protected boolean matchesSafely(Dictionary<String, Object> item) {
+ // use a map for comparison
+ //expectedDictionary.x
+ return expectedDictionary.equals(item);
+ }
+
+ }
+}