From 00b0a42981c118111f4cbbd99d0438ab2679979d Mon Sep 17 00:00:00 2001
From: brinn <brinn>
Date: Tue, 5 Feb 2008 22:01:41 +0000
Subject: [PATCH] add: - unit tests - strategies for duplicate values fix:
 error in iterator()

SVN: 4032
---
 .../collections/TableMapNonUniqueKey.java     |  90 +++++++++--
 .../collections/TableMapNonUniqueKeyTest.java | 142 ++++++++++++++++++
 2 files changed, 221 insertions(+), 11 deletions(-)
 create mode 100644 common/sourceTest/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKeyTest.java

diff --git a/common/source/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKey.java b/common/source/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKey.java
index bb56910bd99..2f39a0d3de2 100644
--- a/common/source/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKey.java
+++ b/common/source/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKey.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 ETH Zuerich, CISD
+ * Copyright 2008 ETH Zuerich, CISD
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -26,16 +26,40 @@ import java.util.Set;
 /**
  * A table of rows of type <code>E</code> with random access via a key of type <code>K</code> where the key does not
  * have to be unique.
+ * <p>
+ * Note that the <i>values</i> still need to be unique (according to the {@link Object#equals(Object)} contract), only
+ * duplicate <i>keys</i> are acceptable for this map.
  * 
  * @author Franz-Josef Elmer
  * @author Bernd Rinn
  */
 public class TableMapNonUniqueKey<K, E> implements Iterable<E>
 {
+    /** Strategy on how to handle unique value constraint violations. */
+    public enum UniqueValueViolationStrategy
+    {
+        KEEP_FIRST, KEEP_LAST, ERROR
+    }
+
+    /**
+     * Exception indicating a violation of the unique value constraint.
+     */
+    public static class UniqueValueViolationException extends RuntimeException
+    {
+        private static final long serialVersionUID = 1L;
+
+        UniqueValueViolationException(String msg)
+        {
+            super(msg);
+        }
+    }
+
     private final Map<K, Set<E>> map = new LinkedHashMap<K, Set<E>>();
 
     private final IKeyExtractor<K, E> extractor;
 
+    private final UniqueValueViolationStrategy uniqueValueViolationStrategy;
+
     /**
      * Creates a new instance for the specified rows and key extractor.
      * 
@@ -43,10 +67,24 @@ public class TableMapNonUniqueKey<K, E> implements Iterable<E>
      * @param extractor Strategy to extract a key of type <code>E</code> for an object of type <code>E</code>.
      */
     public TableMapNonUniqueKey(final Iterable<E> rows, final IKeyExtractor<K, E> extractor)
+    {
+        this(rows, extractor, UniqueValueViolationStrategy.ERROR);
+    }
+
+    /**
+     * Creates a new instance for the specified rows and key extractor.
+     * 
+     * @param rows Collection of rows of type <code>E</code>.
+     * @param extractor Strategy to extract a key of type <code>E</code> for an object of type <code>E</code>.
+     */
+    public TableMapNonUniqueKey(final Iterable<E> rows, final IKeyExtractor<K, E> extractor,
+            UniqueValueViolationStrategy uniqueValueViolationStrategy)
     {
         assert rows != null : "Unspecified collection of rows.";
         assert extractor != null : "Unspecified key extractor.";
+        assert uniqueValueViolationStrategy != null : "Unspecified unique value violation strategy.";
         this.extractor = extractor;
+        this.uniqueValueViolationStrategy = uniqueValueViolationStrategy;
         for (final E row : rows)
         {
             add(row);
@@ -54,23 +92,47 @@ public class TableMapNonUniqueKey<K, E> implements Iterable<E>
     }
 
     /**
-     * Adds the specified row to this table. An already existing row with the same key as <code>row</code> will be
-     * replaced by <code>row</code>.
+     * Adds the specified row to this table. What the method will do when a row is provided that is equals to a row that
+     * is already in the map (according to {@link Object#equals(Object)}, depends on the unique value violation
+     * strategy as given to the constructor:
+     * <ul>
+     * <li>For {@link UniqueValueViolationStrategy#KEEP_FIRST} the first inserted row will be kept and all later ones
+     * will be ignored.</li>
+     * <li>For {@link UniqueValueViolationStrategy#KEEP_LAST} the last inserted row will replace all the others.</li>
+     * <li>For {@link UniqueValueViolationStrategy#ERROR} a {@link UniqueValueViolationException} will be thrown when
+     * trying to insert a row with a key that is already in the map. <i>This is the default.</i>.</li>
+     * </ul>
+     * 
+     * @throws UniqueValueViolationException If a row that equals the <var>row</var> is already in the map and a unique
+     *             value violation strategy of {@link UniqueValueViolationStrategy#ERROR} has been chosen.
      */
-    public final void add(final E row)
+    public final void add(final E row) throws UniqueValueViolationException
     {
         final K key = extractor.getKey(row);
-        Set<E> set = map.get(key); 
+        Set<E> set = map.get(key);
         if (set == null)
         {
             set = new LinkedHashSet<E>();
             map.put(key, set);
+            set.add(row);
+        } else if (uniqueValueViolationStrategy == UniqueValueViolationStrategy.KEEP_FIRST
+                || set.contains(row) == false)
+        {
+            set.add(row);
+        } else if (uniqueValueViolationStrategy == UniqueValueViolationStrategy.KEEP_LAST)
+        {
+            set.remove(row);
+            set.add(row);
+        } else if (uniqueValueViolationStrategy == UniqueValueViolationStrategy.ERROR)
+        {
+            throw new UniqueValueViolationException("Row '" + row.toString() + "' already stored in the map.");
         }
-        set.add(row);
     }
 
     /**
      * Gets the row set for the specified key or <code>null</code> if not found.
+     * <p>
+     * The set is ordered by the order of addition.
      */
     public final Set<E> tryGet(final K key)
     {
@@ -78,21 +140,27 @@ public class TableMapNonUniqueKey<K, E> implements Iterable<E>
     }
 
     /**
-     * Creates an iterator of the rows in the order they have been added. Removing is not supported.
+     * Creates an iterator of the rows. Removing is not supported.
+     * <p>
+     * The order is:
+     * <ol>
+     * <li>Order of addition of the key</li>
+     * <li>Order of the addition of the value for the value's key</li>
+     * </ol>
      */
     public final Iterator<E> iterator()
     {
         return new Iterator<E>()
             {
                 private Iterator<Map.Entry<K, Set<E>>> mapSetIterator = map.entrySet().iterator();
-                
+
                 private Iterator<E> setIterator;
 
                 private boolean setHasNext()
                 {
                     return (setIterator != null) && setIterator.hasNext();
                 }
-                
+
                 public boolean hasNext()
                 {
                     if (setHasNext() == false)
@@ -100,14 +168,14 @@ public class TableMapNonUniqueKey<K, E> implements Iterable<E>
                         if (mapSetIterator.hasNext())
                         {
                             setIterator = mapSetIterator.next().getValue().iterator();
-                        }                        
+                        }
                     }
                     return setHasNext();
                 }
 
                 public E next()
                 {
-                    if (setHasNext() == false)
+                    if (hasNext() == false)
                     {
                         throw new NoSuchElementException("No more elements.");
                     }
diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKeyTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKeyTest.java
new file mode 100644
index 00000000000..25bd8aa9df0
--- /dev/null
+++ b/common/sourceTest/java/ch/systemsx/cisd/common/collections/TableMapNonUniqueKeyTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * Licensed 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 ch.systemsx.cisd.common.collections;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+
+import static org.testng.AssertJUnit.*;
+
+import org.testng.annotations.Test;
+
+/**
+ * Test cases for the {@link TableMapNonUniqueKey}
+ * 
+ * @author Bernd Rinn
+ */
+public class TableMapNonUniqueKeyTest
+{
+
+    final IKeyExtractor<Integer, String> integerExtractor = new IKeyExtractor<Integer, String>()
+        {
+            public Integer getKey(String e)
+            {
+                final int i = e.indexOf(':');
+                if (i >= 0)
+                {
+                    return Integer.parseInt(e.substring(i + 1));
+                } else
+                {
+                    return Integer.parseInt(e);
+                }
+            }
+        };
+
+    @Test
+    public void testIterationUniqueKey()
+    {
+        final TableMapNonUniqueKey<Integer, String> tableMap =
+                new TableMapNonUniqueKey<Integer, String>(Arrays.asList("1", "7", "0"), integerExtractor);
+        Iterator<String> it = tableMap.iterator();
+        assertEquals("1", it.next());
+        assertEquals("7", it.next());
+        assertEquals("0", it.next());
+        assertFalse(it.hasNext());
+    }
+
+    @Test(expectedExceptions = TableMapNonUniqueKey.UniqueValueViolationException.class)
+    public void testIterationDuplicateValuesStrategyError()
+    {
+        new TableMapNonUniqueKey<Integer, String>(Arrays.asList("1", "7", "0", "0", "1"), integerExtractor);
+    }
+
+    @Test
+    public void testIterationDuplicateValuesKeepLastStrategy()
+    {
+        final String null1 = new String(Integer.toString(0));
+        final String null2 = new String(Integer.toString(0));
+        final TableMapNonUniqueKey<Integer, String> tableMap =
+                new TableMapNonUniqueKey<Integer, String>(Arrays.asList("1", "7", null1, null2, "1"), integerExtractor,
+                        TableMapNonUniqueKey.UniqueValueViolationStrategy.KEEP_LAST);
+        Iterator<String> it = tableMap.iterator();
+        assertEquals("1", it.next());
+        assertEquals("7", it.next());
+        final String null3 = it.next();
+        System.out.println(System.identityHashCode(null1) + ":" + System.identityHashCode(null2) + ":"
+                + System.identityHashCode(null3));
+        assertEquals(System.identityHashCode(null2), System.identityHashCode(null3));
+        assertFalse(it.hasNext());
+    }
+
+    @Test
+    public void testIterationDuplicateValuesKeepFirstStrategy()
+    {
+        final String null1 = new String(Integer.toString(0));
+        final String null2 = new String(Integer.toString(0));
+        final TableMapNonUniqueKey<Integer, String> tableMap =
+                new TableMapNonUniqueKey<Integer, String>(Arrays.asList("1", "7", null1, null2, "1"), integerExtractor,
+                        TableMapNonUniqueKey.UniqueValueViolationStrategy.KEEP_FIRST);
+        Iterator<String> it = tableMap.iterator();
+        assertEquals("1", it.next());
+        assertEquals("7", it.next());
+        final String null3 = it.next();
+        System.out.println(System.identityHashCode(null1) + ":" + System.identityHashCode(null2) + ":"
+                + System.identityHashCode(null3));
+        assertEquals(System.identityHashCode(null1), System.identityHashCode(null3));
+        assertFalse(it.hasNext());
+    }
+
+    @Test
+    public void testIterationDuplicateKey()
+    {
+        final TableMapNonUniqueKey<Integer, String> tableMap =
+                new TableMapNonUniqueKey<Integer, String>(Arrays.asList("1", "7", "0", "x:0", "x:1"), integerExtractor);
+        Iterator<String> it = tableMap.iterator();
+        assertEquals("1", it.next());
+        assertEquals("x:1", it.next());
+        assertEquals("7", it.next());
+        assertEquals("0", it.next());
+        assertEquals("x:0", it.next());
+        assertFalse(it.hasNext());
+    }
+
+    @Test
+    public void testTryGet()
+    {
+        final TableMapNonUniqueKey<Integer, String> tableMap =
+                new TableMapNonUniqueKey<Integer, String>(Arrays.asList("1", "7", "0"), integerExtractor);
+        assertNull(tableMap.tryGet(10));
+        assertEquals(Collections.singleton("0"), tableMap.tryGet(0));
+        assertEquals(Collections.singleton("1"), tableMap.tryGet(1));
+        assertEquals(Collections.singleton("7"), tableMap.tryGet(7));
+    }
+
+    @Test
+    public void testTryGetNonUnique()
+    {
+        final TableMapNonUniqueKey<Integer, String> tableMap =
+                new TableMapNonUniqueKey<Integer, String>(Arrays.asList("a:42", "7", "b:42", "0", "b:7", "c:42"),
+                        integerExtractor);
+        assertNull(tableMap.tryGet(10));
+        assertEquals(Collections.singleton("0"), tableMap.tryGet(0));
+        assertEquals(new HashSet<String>(Arrays.asList("7", "b:7")), tableMap.tryGet(7));
+        assertEquals(new HashSet<String>(Arrays.asList("a:42", "b:42", "c:42")), tableMap.tryGet(42));
+    }
+
+}
-- 
GitLab