This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 553261974bb73309eb0436c47fe5f3cff12ca41e
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Feb 15 14:57:46 2022 +0100

    Implement more optional methods in `WeakHashMap`.
---
 .../sis/util/collection/WeakValueHashMap.java      | 140 ++++++++++++++++++---
 .../sis/util/collection/WeakValueHashMapTest.java  |  47 +++++++
 2 files changed, 170 insertions(+), 17 deletions(-)

diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
 
b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
index e01b390..617bad1 100644
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
@@ -72,7 +72,7 @@ import static org.apache.sis.util.collection.WeakEntry.*;
  * then the caller can synchronize on {@code this}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.7
+ * @version 1.2
  *
  * @param <K>  the class of key elements.
  * @param <V>  the class of value elements.
@@ -390,13 +390,43 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
     }
 
     /**
-     * Implementation of {@link #put(Object, Object)} and {@link 
#remove(Object)} operations
+     * Wildcard for {@link #intern(Object, Object, Object)} condition meaning 
whether a key shall be associated
+     * to a value or not. Note that {@link #equals(Object)} and {@link 
#hashCode()} methods are inconsistent in
+     * this class; the {@code hashCode()} method should never be invoked.
+     */
+    @SuppressWarnings("overrides")
+    private static final class Wildcard {
+        static final Wildcard ANY_VALUE = new Wildcard(true);
+        static final Wildcard NO_VALUE  = new Wildcard(false);
+
+        /** Whether the key shall be associated to a value. */
+        private final boolean present;
+
+        /** Creates the {@link #ANY_VALUE} or {@link #NO_VALUE} constant. */
+        private Wildcard(final boolean present) {
+            this.present = present;
+        }
+
+        /** Tests for the {@link #ANY_VALUE} or {@link #NO_VALUE} condition. */
+        @Override public boolean equals(Object oldValue) {
+            return (oldValue != null) == present;
+        }
+    }
+
+    /**
+     * Implementation of {@link #put(Object, Object)}, {@link 
#putIfAbsent(Object, Object)}, {@link #remove(Object)},
+     * {@link #replace(Object, Object)} and {@link #replace(Object, Object, 
Object)} operations.
+     *
+     * @param  key        key with which the specified value is to be 
associated.
+     * @param  value      value to be associated with the specified key, or 
{@code null} for removing the entry.
+     * @param  condition  previous value that entry must have for doing the 
action, or {@code null} if no restriction.
+     * @return the previous value associated with specified key, or {@code 
null} if there was no mapping for the key.
      */
     @SuppressWarnings("unchecked")
-    private synchronized V intern(final Object key, final V value, final 
boolean replace) {
+    private synchronized V intern(final Object key, final V value, final 
Object condition) {
         assert isValid();
         /*
-         * If 'value' is already contained in this WeakValueHashMap, we need 
to clear it.
+         * If `value` is already contained in this WeakValueHashMap, we need 
to clear it.
          */
         V oldValue = null;
         Entry[] table = this.table;
@@ -405,15 +435,20 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
         for (Entry e = table[index]; e != null; e = (Entry) e.next) {
             if (keyEquals(key, e.key)) {
                 oldValue = e.get();
-                if (oldValue != null && !replace) {
+                if (condition != null && !condition.equals(oldValue)) {
                     return oldValue;
                 }
                 e.dispose();
-                table = this.table; // May have changed.
+                table = this.table;             // May have changed.
                 index = hash % table.length;
             }
         }
-        if (value != null) {
+        /*
+         * If a value has been specified, add it after above removal of 
previous value except if
+         * this method is invoked from `replace(key, old)` (condition = 
`Wildcard.ANY_VALUE`) or
+         * `replace(key, old, new)` (condition = valid object) and no previous 
value was mapped.
+         */
+        if (value != null && (condition == null || condition == 
Wildcard.NO_VALUE || oldValue != null)) {
             if (++count >= lowerCapacityThreshold(table.length)) {
                 if (count > upperCapacityThreshold(table.length)) {
                     this.table = table = (Entry[]) rehash(table, count, "put");
@@ -433,17 +468,16 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
      *
      * @param  key    key with which the specified value is to be associated.
      * @param  value  value to be associated with the specified key.
-     * @return the previous value associated with specified key, or {@code 
null} if there was no mapping for key.
-     *
+     * @return the previous value associated with specified key, or {@code 
null} if there was no mapping for the key.
      * @throws NullArgumentException if the key or the value is {@code null}.
      */
     @Override
-    public V put(final K key, final V value) throws NullArgumentException {
+    public V put(final K key, final V value) {
         if (key == null || value == null) {
             throw new NullArgumentException(Errors.format(key == null
                     ? Errors.Keys.NullMapKey : Errors.Keys.NullMapValue));
         }
-        return intern(key, value, true);
+        return intern(key, value, null);
     }
 
     /**
@@ -454,30 +488,102 @@ public class WeakValueHashMap<K,V> extends 
AbstractMap<K,V> {
      *
      * @param  key    key with which the specified value is to be associated.
      * @param  value  value to be associated with the specified key.
-     * @return the current value associated with specified key, or {@code 
null} if there was no mapping for key.
-     *
+     * @return the current value associated with specified key, or {@code 
null} if there was no mapping for the key.
      * @throws NullArgumentException if the key or the value is {@code null}.
      *
      * @since 0.7
      */
     @Override
-    public V putIfAbsent(final K key, final V value) throws 
NullArgumentException {
+    public V putIfAbsent(final K key, final V value) {
         if (key == null || value == null) {
             throw new NullArgumentException(Errors.format(key == null
                     ? Errors.Keys.NullMapKey : Errors.Keys.NullMapValue));
         }
-        return intern(key, value, false);
+        return intern(key, value, Wildcard.NO_VALUE);
+    }
+
+    /**
+     * Replaces the entry for the specified key only if it is currently mapped 
to some value.
+     *
+     * @param  key    key with which the specified value is to be associated.
+     * @param  value  value to be associated with the specified key.
+     * @return the previous value associated with specified key, or {@code 
null} if there was no mapping for the key.
+     * @throws NullArgumentException if the value is {@code null}.
+     *
+     * @since 1.2
+     */
+    @Override
+    public V replace(final K key, final V value) {
+        if (value == null) {
+            throw new 
NullArgumentException(Errors.format(Errors.Keys.NullMapValue));
+        }
+        if (key == null) return null;
+        return intern(key, value, Wildcard.ANY_VALUE);
+    }
+
+    /**
+     * Replaces the entry for the specified key only if currently mapped to 
the specified value.
+     *
+     * @param  key       key with which the specified value is to be 
associated.
+     * @param  oldValue  value expected to be associated with the specified 
key.
+     * @param  newValue  value to be associated with the specified key.
+     * @return {@code true} if the value was replaced.
+     * @throws NullArgumentException if the new value is {@code null}.
+     *
+     * @since 1.2
+     */
+    @Override
+    public boolean replace(final K key, final V oldValue, final V newValue) {
+        if (newValue == null) {
+            throw new 
NullArgumentException(Errors.format(Errors.Keys.NullMapValue));
+        }
+        return replaceOrRemove(key, oldValue, newValue);
     }
 
     /**
      * Removes the mapping for this key from this map if present.
      *
      * @param  key  key whose mapping is to be removed from the map.
-     * @return previous value associated with specified key, or {@code null} 
if there was no entry for key.
+     * @return previous value associated with specified key, or {@code null} 
if there was no entry for the key.
      */
     @Override
     public V remove(final Object key) {
-        return intern(key, null, true);
+        if (key == null) return null;
+        return intern(key, null, null);
+    }
+
+    /**
+     * Removes the entry for the specified key only if it is currently mapped 
to the specified value.
+     *
+     * @param  key    key whose mapping is to be removed from the map.
+     * @param  value  value expected to be associated with the specified key.
+     * @return {@code true} if the value was removed.
+     *
+     * @since 1.2
+     */
+    @Override
+    public boolean remove(final Object key, final Object value) {
+        return replaceOrRemove(key, value, null);
+    }
+
+    /**
+     * Implementation of {@link #replace(Object, Object, Object)} and {@link 
#remove(Object, Object)}.
+     * The replace action has a non-null {@code newValue} and the remove 
action has a null new value.
+     */
+    private boolean replaceOrRemove(final Object key, final Object oldValue, 
final V newValue) {
+        if (key == null || oldValue == null) {
+            return false;
+        }
+        @SuppressWarnings("overrides")
+        final class Observer {
+            boolean equals;
+
+            @Override public boolean equals(final Object other) {
+                return equals = oldValue.equals(other);
+            }
+        }
+        final Observer observer = new Observer();
+        return intern(key, newValue, observer) != null && observer.equals;
     }
 
     /**
diff --git 
a/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java
 
b/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java
index 2053aad..aac2160 100644
--- 
a/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java
+++ 
b/core/sis-utility/src/test/java/org/apache/sis/util/collection/WeakValueHashMapTest.java
@@ -23,6 +23,7 @@ import org.apache.sis.test.TestCase;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.TestConfiguration;
+import org.apache.sis.test.TestUtilities;
 import org.junit.Test;
 
 import static org.apache.sis.test.Assert.*;
@@ -216,4 +217,50 @@ public final strictfp class WeakValueHashMapTest extends 
TestCase {
         assertSame(v2, weakMap.get(k2));
         assertSame(v3, weakMap.get(k3));
     }
+
+    /**
+     * Tests {@code putIfAbsent(…)}, {@code replace(…)} and other optional 
methods.
+     */
+    @Test
+    public void testOptionalMethods() {
+        final WeakValueHashMap<Integer,Integer> weakMap = new 
WeakValueHashMap<>(Integer.class);
+        final HashMap<Integer,Integer> reference = new HashMap<>();
+        final Random random = TestUtilities.createRandomNumberGenerator();
+        for (int i=0; i<100; i++) {
+            final Integer key   = random.nextInt(10);
+            final Integer value = random.nextInt(20);
+            switch (random.nextInt(7)) {
+                case 0: {
+                    assertEquals(reference.get(key), weakMap.get(key));
+                    break;
+                }
+                case 1: {
+                    assertEquals(reference.put(key, value), weakMap.put(key, 
value));
+                    break;
+                }
+                case 2: {
+                    assertEquals(reference.putIfAbsent(key, value), 
weakMap.putIfAbsent(key, value));
+                    break;
+                }
+                case 3: {
+                    assertEquals(reference.replace(key, value), 
weakMap.replace(key, value));
+                    break;
+                }
+                case 4: {
+                    final Integer condition = random.nextInt(20);
+                    assertEquals(reference.replace(key, condition, value), 
weakMap.replace(key, condition, value));
+                    break;
+                }
+                case 5: {
+                    assertEquals(reference.remove(key), weakMap.remove(key));
+                    break;
+                }
+                case 6: {
+                    assertEquals(reference.remove(key, value), 
weakMap.remove(key, value));
+                    break;
+                }
+            }
+        }
+        assertMapEquals(reference, weakMap);
+    }
 }

Reply via email to