SLING-9140 - utility to merge multiple value maps (#21)

SLING-9140 - utility to merge multiple value maps

- introduce o.a.s.api.wrapper.ValueMapUtil
- support merging of multiple ValueMaps
- support caching of a ValueMap
- deprecate CompositeValueMap in favour of ValueMapUtil#merge

Co-authored-by: Nicolas Peltier <1032754+npeltier@users.noreply.github.com>
diff --git a/pom.xml b/pom.xml
index 8881b65..fbabce1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
     </parent>
 
     <artifactId>org.apache.sling.api</artifactId>
-    <version>2.22.1-SNAPSHOT</version>
+    <version>2.23.0-SNAPSHOT</version>
 
     <name>Apache Sling API</name>
     <description>
diff --git a/src/main/java/org/apache/sling/api/wrappers/CompositeValueMap.java b/src/main/java/org/apache/sling/api/wrappers/CompositeValueMap.java
index cee8cca..4f90022 100644
--- a/src/main/java/org/apache/sling/api/wrappers/CompositeValueMap.java
+++ b/src/main/java/org/apache/sling/api/wrappers/CompositeValueMap.java
@@ -36,8 +36,14 @@
  * In case you would like to avoid duplicating properties on multiple resources,
  * you can use a <code>CompositeValueMap</code> to get a concatenated map of
  * properties.
+ * 
  * @since 2.3 (Sling API Bundle 2.5.0)
+ *
+ * @deprecated Use {@link ValueMapUtil#merge(ValueMap...)} instead. Note that it
+ * does not support the parameter {@code merge = false}. However, this could easily
+ * be achieved with another decorator that restricts the set of allowed keys.
  */
+@Deprecated
 public class CompositeValueMap implements ValueMap {
 
     /**
diff --git a/src/main/java/org/apache/sling/api/wrappers/ValueMapUtil.java b/src/main/java/org/apache/sling/api/wrappers/ValueMapUtil.java
new file mode 100644
index 0000000..5283a32
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/wrappers/ValueMapUtil.java
@@ -0,0 +1,97 @@
+/*
+ * 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.api.wrappers;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.impl.CachingValueMap;
+import org.apache.sling.api.wrappers.impl.MergingValueMap;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+public final class ValueMapUtil {
+
+    /**
+     * A convenience method that turns the var-args into a {@code Collection}
+     * and delegates to {@link #merge(List)}.
+     *
+     * @param valueMaps the {@code ValueMap} instances to merge
+     * @return the merged {@code ValueMap} view
+     *
+     * @see #merge(List)
+     */
+    @NotNull
+    public static ValueMap merge(@NotNull ValueMap... valueMaps) {
+        return merge(asList(valueMaps));
+    }
+
+    /**
+     * Merge provided {@code ValueMaps} into a single view {@code ValueMap} that aggregates
+     * all key-value pairs of the given maps. The value for a key-value pair is taken from
+     * the first {@code ValueMap} (in iteration order) that has a mapping for the given key.
+     * <br>
+     * E.g. assuming {@code merge(vm1, vm2, vm3} where all maps {@code vm1, vm2, vm3} have
+     * a value mapped to the key {@code k1}, then the value from {@code vm1} is returned.
+     *
+     * @param valueMaps the {@code ValueMap} instances to merge
+     * @return the merged {@code ValueMap} view
+     */
+    @NotNull
+    public static ValueMap merge(@NotNull List<ValueMap> valueMaps) {
+        return new MergingValueMap(valueMaps);
+    }
+
+    /**
+     * Convenience method that allows creating a merged {@code ValueMap} where
+     * accessed mappings are cached to optimize repeated lookups.
+     * <br>
+     * This is equivalent to calling {@code cache(merge(valueMaps))}.
+     *
+     * @param valueMaps the {@code ValueMap} instances to merge
+     * @return the merged and cached {@code ValueMap} view
+     */
+    @NotNull
+    public static ValueMap mergeAndCache(@NotNull List<ValueMap> valueMaps) {
+        return cache(merge(valueMaps));
+    }
+
+    /**
+     * Decorates the given {@code ValueMap} with a caching layer.
+     * Every key-value pair that is accessed is cached for
+     * subsequent accesses. Calls to {@code ValueMap#keySet()},
+     * {@code ValueMap#values()} and {@code ValueMap#entrySet()}
+     * will cause all entries to be cached.
+     * <br>
+     * Note: if the underlying {@code ValueMap} is modified, the
+     * modification may not be reflected via the caching wrapper.
+     *
+     * @param valueMap the {@code ValueMap} instance to cache
+     * @return the cached {@code ValueMap} view
+     */
+    @NotNull
+    public static ValueMap cache(@NotNull ValueMap valueMap) {
+        return new CachingValueMap(valueMap);
+    }
+    
+    /**
+     * private constructor to hide implicit public one
+     */
+    private ValueMapUtil() {
+    }
+}
diff --git a/src/main/java/org/apache/sling/api/wrappers/impl/CachingValueMap.java b/src/main/java/org/apache/sling/api/wrappers/impl/CachingValueMap.java
new file mode 100644
index 0000000..9a7ae0f
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/wrappers/impl/CachingValueMap.java
@@ -0,0 +1,123 @@
+/*
+ * 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.api.wrappers.impl;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ValueMap decorator that caches key-value pairs that were accessed before.
+ *
+ * @see ValueMapUtil#cache(org.apache.sling.api.resource.ValueMap)
+ */
+public class CachingValueMap implements ValueMap {
+
+    private static final String IMMUTABLE_ERROR_MESSAGE = "CachingValueMap is immutable";
+
+    private final ValueMap delegate;
+
+    private final Map<String, Object> cache = new HashMap<>();
+
+    private boolean fullyCached = false;
+
+    public CachingValueMap(ValueMap delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public int size() {
+        return fullyCached ? cache.size() : delegate.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return cache.containsKey(key) || delegate.containsKey(key);
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return cache.containsValue(value) || delegate.containsValue(value);
+    }
+
+    @Override
+    public Object get(Object key) {
+        return key instanceof String ? cache.computeIfAbsent((String)key, delegate::get) : null;
+    }
+
+    @NotNull
+    @Override
+    public Set<String> keySet() {
+        ensureFullyCached();
+        return cache.keySet();
+    }
+
+    @NotNull
+    @Override
+    public Collection<Object> values() {
+        ensureFullyCached();
+        return cache.values();
+    }
+
+    @NotNull
+    @Override
+    public Set<Entry<String, Object>> entrySet() {
+        ensureFullyCached();
+        return cache.entrySet();
+    }
+
+    private void ensureFullyCached() {
+        if (!fullyCached) {
+            cache.putAll(delegate);
+            fullyCached = true;
+        }
+    }
+
+    @Nullable
+    @Override
+    public Object put(String key, Object value) {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+
+    @Override
+    public Object remove(Object key) {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+
+    @Override
+    public void putAll(@NotNull Map<? extends String, ?> m) {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+
+    @Override
+    public void clear() {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+}
diff --git a/src/main/java/org/apache/sling/api/wrappers/impl/MergingValueMap.java b/src/main/java/org/apache/sling/api/wrappers/impl/MergingValueMap.java
new file mode 100644
index 0000000..9e2bef3
--- /dev/null
+++ b/src/main/java/org/apache/sling/api/wrappers/impl/MergingValueMap.java
@@ -0,0 +1,177 @@
+/*
+ * 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.api.wrappers.impl;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Merge provided {@code ValueMaps} into a single view {@code ValueMap} that aggregates
+ * all key-value pairs of the given maps. The value for a key-value pair is taken from
+ * the first {@code ValueMap} (in iteration order) that has a mapping for the given key.
+ * <br>
+ * In case you would like to avoid duplicating properties on multiple resources,
+ * you can use a <code>{@link MergingValueMap}</code> to get a concatenated map of
+ * properties.
+ *
+ * @see ValueMapUtil#merge(List)
+ * @see ValueMapUtil#merge(ValueMap...)
+ * @see ValueMapUtil#mergeAndCache(List)
+ */
+public class MergingValueMap implements ValueMap {
+
+    private static final String IMMUTABLE_ERROR_MESSAGE = "MergingValueMap is immutable";
+
+    private final List<ValueMap> valueMaps;
+
+    /**
+     * Constructor that allows merging any number of {@code ValueMap}
+     * instances into a single {@code ValueMap} view. The keys of the
+     * view are the union of the keys of all value maps. The values of
+     * the view is the mapping of all keys to their respective value.
+     * The entries are the key-value pairs. Values are retrieved by
+     * getting the value for a key for each {@code ValueMap} until a
+     * non-null value is found.
+     *
+     * @param valueMaps The ValueMaps to be merged.
+     *
+     * @see ValueMapUtil#merge(List)
+     * @see ValueMapUtil#merge(ValueMap...)
+     */
+    public MergingValueMap(@NotNull List<ValueMap> valueMaps) {
+        this.valueMaps = Collections.unmodifiableList(new ArrayList<>(valueMaps));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int size() {
+        return keySet().size();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsKey(final Object key) {
+        return keyStream().anyMatch(k -> Objects.equals(k, key));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsValue(final Object value) {
+        return valueStream().anyMatch(v -> Objects.equals(v, value));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object get(Object key) {
+        return valueMaps.stream()
+                .map(vm -> vm.get(key))
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElse(null);
+    }
+
+    @NotNull
+    @Override
+    public Set<String> keySet() {
+        return keyStream().collect(Collectors.toSet());
+    }
+
+    @NotNull
+    @Override
+    public Collection<Object> values() {
+        return valueStream().collect(Collectors.toList());
+    }
+
+    @NotNull
+    @Override
+    public Set<Entry<String, Object>> entrySet() {
+        return keyStream()
+                .map(key -> new AbstractMap.SimpleEntry<>(key, get(key)))
+                .collect(Collectors.toSet());
+    }
+
+    @NotNull
+    private Stream<String> keyStream() {
+        return valueMaps.stream()
+                .map(Map::keySet)
+                .flatMap(Collection::stream)
+                .distinct();
+    }
+
+    @NotNull
+    private Stream<Object> valueStream() {
+        return keyStream().map(this::get);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object put(final String aKey, final Object value) {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object remove(final Object key) {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void putAll(final Map<? extends String, ?> properties) {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void clear() {
+        throw new UnsupportedOperationException(IMMUTABLE_ERROR_MESSAGE);
+    }
+}
diff --git a/src/main/java/org/apache/sling/api/wrappers/package-info.java b/src/main/java/org/apache/sling/api/wrappers/package-info.java
index aa18721..39ff070 100644
--- a/src/main/java/org/apache/sling/api/wrappers/package-info.java
+++ b/src/main/java/org/apache/sling/api/wrappers/package-info.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-@Version("2.6.4")
+@Version("2.7.0")
 package org.apache.sling.api.wrappers;
 
 import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/sling/api/resource/impl/ValueMapUtilMergeTest.java b/src/test/java/org/apache/sling/api/resource/impl/ValueMapUtilMergeTest.java
new file mode 100644
index 0000000..470955a
--- /dev/null
+++ b/src/test/java/org/apache/sling/api/resource/impl/ValueMapUtilMergeTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.api.resource.impl;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapUtil;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class ValueMapUtilMergeTest {
+
+    private final Function<Collection<ValueMap>, ValueMap> mergeFn;
+
+    @Parameterized.Parameters(name = "using ValueMapUtil#{0}")
+    public static Iterable<Object[]> testedMergeMethod() {
+        return asList(
+                new Object[] { "mergeAndCache", (Function<List<ValueMap>, ValueMap>)ValueMapUtil::mergeAndCache},
+                new Object[] { "merge", (Function<List<ValueMap>, ValueMap>)ValueMapUtil::merge}
+        );
+    }
+
+    public ValueMapUtilMergeTest(String name, Function<Collection<ValueMap>, ValueMap> mergeFn) {
+        this.mergeFn = mergeFn;
+    }
+
+    private ValueMap merge(ValueMap... valueMaps) {
+        return mergeFn.apply(asList(valueMaps));
+    }
+
+    @Test
+    public void isEmptyTest() {
+        ValueMap vm = merge(createValueMap(), createValueMap());
+        assertTrue("Value map should be empty", vm.isEmpty());
+        assertFalse("Typical map should not be empty", typicalVM().isEmpty());
+    }
+
+    @Test
+    public void keySetTest() {
+        assertThat("key set should be k1-k5", typicalVM().keySet(), containsInAnyOrder("k1", "k2", "k3", "k4", "k5"));
+    }
+
+    @Test
+    public void valuesTest() {
+        assertThat("values should be the one expected", typicalVM().values(), containsInAnyOrder("11", "22", "13", "24", "35"));
+    }
+
+    @Test
+    public void testGet() {
+        ValueMap vm = typicalVM();
+        assertEquals("k1-11", "11", vm.get("k1"));
+        assertEquals("k2-22", "22", vm.get("k2"));
+        assertEquals("k3-13", "13", vm.get("k3"));
+        assertEquals("k4-24", "24", vm.get("k4"));
+        assertEquals("k5-5", "35", vm.get("k5"));
+    }
+
+    @Test
+    public void testNullGet() {
+        assertNull("null get should return null", typicalVM().get(null));
+    }
+
+    @Test
+    public void testDefaultValue() {
+        ValueMap vm = typicalVM();
+        String randomString = "not-that-random-string";
+        assertEquals("default value should work", randomString, vm.get("random-key", randomString));
+    }
+
+    @Test
+    public void testContainsKey() {
+        ValueMap vm = typicalVM();
+        assertTrue("should contain 11", vm.containsValue("11"));
+        assertFalse("should not contain 21", vm.containsValue("21"));
+    }
+
+    @Test
+    public void testContainsValue() {
+        ValueMap vm = typicalVM();
+        assertTrue("should contain k1", vm.containsKey("k1"));
+        assertTrue("should contain k2", vm.containsKey("k2"));
+        assertTrue("should contain k3", vm.containsKey("k3"));
+        assertTrue("should contain k4", vm.containsKey("k4"));
+        assertTrue("should contain k5", vm.containsKey("k5"));
+    }
+
+    @Test
+    public void testDeepGet() {
+        ValueMap vm = merge(createValueMap("k/1", "11"), createValueMap("1", "21"));
+        assertEquals("value should be 11", "11", vm.get("k/1", String.class));
+    }
+
+    @Test
+    public void testLong() {
+        ValueMap vm = merge(createValueMap("k", 11L));
+        assertEquals("result should be the same than a simple property fetching", "11", vm.get("k", String.class));
+        assertEquals("result should be the same than a simple property fetching", 11L, Objects.requireNonNull(vm.get("k", Long.class)).longValue());
+    }
+
+    @Test
+    public void testBoolean() {
+        ValueMap vm = merge(createValueMap("k", true));
+        assertEquals("result should be the same than a simple property fetching", "true", vm.get("k", String.class));
+        assertEquals("result should be the same than a simple property fetching", true, vm.get("k", boolean.class));
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testPutOnVM() {
+        typicalVM().put("foo", "bar");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testRemoveOnVM() {
+        typicalVM().remove("foo");
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testPutAllOnVM() {
+        typicalVM().putAll(typicalVM());
+    }
+
+    @Test(expected = UnsupportedOperationException.class)
+    public void testClearOnVM() {
+        typicalVM().clear();
+    }
+
+    private static ValueMap createValueMap(Object... pairs) {
+        ValueMap vm = new ValueMapDecorator(new HashMap<>());
+        for (int i = 0; i < pairs.length; i += 2) {
+            vm.put(pairs[i].toString(), pairs[i + 1]);
+        }
+        return vm;
+    }
+
+    /**
+     * @return typical vm, with 3 inner resources, sharing properties from k1 to k5, noted kX each level Y's value
+     * being YX
+     */
+    private ValueMap typicalVM() {
+        ValueMap v1 = createValueMap("k1", "11", "k3", "13");
+        ValueMap v2 = createValueMap("k1", "21", "k2", "22", "k4", "24");
+        ValueMap v3 = createValueMap("k1", "31", "k3", "33", "k4", "34", "k5", "35");
+        return merge(v1, v2, v3);
+    }
+}
diff --git a/src/test/java/org/apache/sling/api/wrappers/CompositeValueMapTest.java b/src/test/java/org/apache/sling/api/wrappers/CompositeValueMapTest.java
index 5728a16..154055b 100644
--- a/src/test/java/org/apache/sling/api/wrappers/CompositeValueMapTest.java
+++ b/src/test/java/org/apache/sling/api/wrappers/CompositeValueMapTest.java
@@ -18,15 +18,16 @@
  */
 package org.apache.sling.api.wrappers;
 
+import org.apache.sling.api.resource.ValueMap;
+import org.junit.Assert;
+import org.junit.Test;
+
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
-import org.apache.commons.collections.CollectionUtils;
-import org.apache.sling.api.resource.ValueMap;
-import org.junit.Assert;
-import org.junit.Test;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 
 public class CompositeValueMapTest {
 
@@ -168,7 +169,7 @@
                 }
 
                 if (testResult.shouldHaveNewType()) {
-                    Assert.assertTrue("Type of property '" + property + "' should have changed", valueMap.get(property).getClass().equals(testResult.expectedNewType));
+                    Assert.assertEquals("Type of property '" + property + "' should have changed",testResult.expectedNewType, valueMap.get(property).getClass());
                     expectedMap.put(property, testResult.extendedValue);
                 }
 
@@ -180,9 +181,9 @@
         }
 
         Assert.assertEquals("Final map size does NOT match", expectedSize, valueMap.size());
-        Assert.assertEquals("Final map entries do NOT match", expectedMap.entrySet(), valueMap.entrySet());
-        Assert.assertEquals("Final map keys do NOT match", expectedMap.keySet(), valueMap.keySet());
-        Assert.assertTrue("Final map values do NOT match expected: <" + expectedMap.values() + "> but was: <" + valueMap.values() + ">", CollectionUtils.isEqualCollection(expectedMap.values(), valueMap.values()));
+        Assert.assertThat("Final map keys do NOT match", valueMap.keySet(), containsInAnyOrder(expectedMap.keySet().toArray()));
+        Assert.assertThat("Final map values do NOT match", valueMap.values(), containsInAnyOrder(expectedMap.values().toArray()));
+        Assert.assertThat("Final map entries do NOT match", valueMap.entrySet(), containsInAnyOrder(expectedMap.entrySet().toArray()));
     }
 
     /**