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 bb56910bd996b21786579401bb86c3f7312fa607..2f39a0d3de28ae68ad73558d06c90e7a9ca725d6 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 0000000000000000000000000000000000000000..25bd8aa9df01ae3c3d2377f96c2f792440f419a5 --- /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)); + } + +}