> 'Record' confused me and made me think of databases. Is > KeyValueHistory a better name?
Stephen, you're probably right. I had been bouncing back and forth between 'Record' and 'History'... the data structure does *record* a kv-pair's *history*, but in fact only the most recent part (one state transition 'record') of the history. However, one would expect that a "KeyValueHistory" class should be able to store the entire history of a key (as well as just a single value). I hadn't initially needed this capability for my own purposes, but I now see the usefulness. So, #getPreviousValue : Object should probably be replaced with: #getAllValues : List What do we think? In terms of design, either the constructors would get very messy to use, or we need to add factory methods to distinguish between: KeyValueHistory( Object key, Object value) KeyValueHistory( Object key, List values) Attached is an implementation that supports multiple previous values, and provides static factory methods. Thoughts? - Neil
/* * $Header: x:/apps/cvsnt/cvs_repository/main/notifyingcollections/src/java/org/apache/commons/collections/KeyValueRecord.java,v 1.2 2003/09/20 22:00:32 otoolen Exp $ * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 2001-2003 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowledgment: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "The Jakarta Project", "Commons", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.commons.collections; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; /** * An immutable {key, value, previous-value} triplet. This class is frequently used * in conjunction with <code>Map.Entry</code>. A constructor is provided to create * a <code>KeyValueRecord</code> from a <code>Map.Entry</code>, and the [EMAIL PROTECTED] #asMapEntry} * method can be used to view an object of this class as a <code>Map.Entry</code>. * Note that it is not possible for <code>KeyValueRecord</code> to implement the <code>Map.Entry</code> * interface as the <code>#equals</code> implementations are not compatible. * * * * @author Neil O'Toole */ public class KeyValueHistory { /** * Create a new <code>KeyValueHistory</code> with supplied key and value, * and no previous values. */ public static KeyValueHistory createFromKeyValue(final Object key, final Object value) { return new KeyValueHistory(key, Collections.singletonList(value)); } /** * Create a new <code>KeyValueHistory</code> with the specified key and values. * The supplied list must: * <ul> * <li>contain at least one value</li> * <li>contain the values in reverse-chronological order (i.e. most * recent value first)</li> * <li>be immutable</li> * </ul> * * @param values an immutable list of the values associated with this key. * @throws IllegalArgumentException if <code>values</code> is <code>null</code> or * is empty. <br />Note: If the value associated with the key is the value <code>null</code>, * then supply <code>Collections#singletonList( null )</code>. */ public static KeyValueHistory createFromKeyValueList(final Object key, final List values) { if (values == null || values.size() < 1) { throw new IllegalArgumentException("The supplied 'values' list must contain at least one value."); } return new KeyValueHistory(key, values); } /** * Create a new <code>KeyValueHistory</code> with supplied key, value * and previous value. */ public static KeyValueHistory createFromKeyValuePrevious(final Object key, final Object value, final Object previous) { final List values = new ArrayList(2); values.add(value); values.add(previous); return new KeyValueHistory(key, Collections.unmodifiableList(values)); } /** * Create a new <code>KeyValueHistory</code> with key and value from the supplied * <code>Map.Entry</code> and no previous value. */ public static KeyValueHistory createFromMapEntry(final Map.Entry entry) { return new KeyValueHistory(entry.getKey(), Collections.singletonList(entry.getValue())); } private int hash = -1; // lazily calculated private final Object key; private final List values; /** * Create a new <code>KeyValueHistory</code> with the specified key and values. * The supplied list: * <ul> * <li>must contain at least one value</li> * <li>must contain the values in reverse-chronological order (i.e. most * recent value first)</li> * <li>must be immutable</li> * * @param values an immutable List of the values associated with this key. */ protected KeyValueHistory(final Object key, final List values) { this.key = key; this.values = values; } /** * Returns a [EMAIL PROTECTED] Map.Entry} view of the supplied <code>KeyValueRecord</code>. The * returned entry is unmodifiable (<code>#setValue</code> throws an [EMAIL PROTECTED] UnsupportedOperationException}), * as the backing <code>KeyValueRecord</code> is itself immutable. * The returned entry correctly implements the <code>#hashCode</code> and * <code>#equals</code> operations as per the <code>Map.Entry</code> contract. */ public Map.Entry asMapEntry() { return new Map.Entry() { private int hash = -1; public boolean equals(Object o) { if (o instanceof Map.Entry == false) { return false; } if (o == this) { return true; } Map.Entry e = (Map.Entry) o; return ( KeyValueHistory.this.getKey() == null ? e.getKey() == null : KeyValueHistory.this.getKey().equals(e.getKey())) && (KeyValueHistory.this.getValue() == null ? e.getValue() == null : KeyValueHistory.this.getValue().equals(e.getValue())); } public Object getKey() { return KeyValueHistory.this.getKey(); } public Object getValue() { return KeyValueHistory.this.getValue(); } public int hashCode() { if (this.hash == -1) { this.hash = (KeyValueHistory.this.getKey() == null ? 0 : KeyValueHistory.this.getKey().hashCode()) ^ (KeyValueHistory.this.getValue() == null ? 0 : KeyValueHistory.this.getValue().hashCode()); } return this.hash; } public Object setValue(Object value) { throw new UnsupportedOperationException("This Map.Entry is unmodifiable."); } public String toString() { return new StringBuffer() .append(KeyValueHistory.this.getKey()) .append('=') .append(KeyValueHistory.this.getValue()) .toString(); } }; } /** * Compares the specified object with this <code>KeyValueRecord</code> for equality. Returns * true if the given object is also a <code>KeyValueRecord</code> and * the records' key, value, and previous value are equal. * * @param o object to be compared for equality with this <code>KeyValueRecord</code>. * @return <code>true</code> if the specified object is equal to this * record. */ public boolean equals(final Object o) { if (!(o instanceof KeyValueHistory)) { return false; } if (this == o) { return true; } final KeyValueHistory kvr = (KeyValueHistory) o; return (this.key == null ? kvr.key == null : this.key.equals(kvr.key)) && this.values.equals(kvr.values); } /** * Returns the first value previously associated with this key, if any. Note that * <code>null</code> will be returned * if the previous value is <code>null</code> <i>or</i> if there are no previous values * Therefore [EMAIL PROTECTED] #hasPreviousValues()} should be used to test if there is a previous value. * * @return the previous value (which may be <code>null</code>) * associated with this record's key, or <code>null</code> if there is no * previous value. */ /* public Object getPreviousValue() { return (this.values.size() < 2) ? null : this.values.get(1); }*/ /* public List getAllPreviousValues() { return this.values.subList(1, values.size() -1); } */ /** * Return all values associated with this key, in reverse chronological * order. The most recent (current) value is at index zero, the previous value is * at index one, the value previous to that at index two, etc.. If there are no * previous values associated with this key, then list will contain only one element (the current value). * The returned list is immutable. * * @return an immutable list of the values associated with this key, the list having a minimum size of one. */ public List getAllValues() { return this.values; } /** * Returns the key associated with this record. */ public Object getKey() { return this.key; } /** * Returns the current (or most recent) value associated with this record's key. * * @return the value (which may be <code>null</code>) * associated with this record's key */ public Object getValue() { return this.values.get(0); } public int hashCode() { if (this.hash == -1) { this.hash = (this.key == null ? 0 : this.key.hashCode()) ^ this.values.hashCode(); } return this.hash; } /** * Returns true if this record's key * was associated with a value previous to being * associated with its current value. * * @return <code>true</code> if this key previously had a value associated * with it, <code>false</code> otherwise. * @see #getPreviousValue() */ public boolean hasPreviousValues() { return this.values.size() > 1; } /** * Returns a string representation of this <code>KeyValueHistory</code>. * */ public String toString() { final StringBuffer sb = new StringBuffer().append(this.key).append('=').append(this.values); return sb.toString(); } }
/* * $Header: x:/apps/cvsnt/cvs_repository/main/notifyingcollections/src/test/org/apache/commons/collections/TestKeyValueHistory.java,v 1.1 2003/09/20 22:00:46 otoolen Exp $ * ==================================================================== * * The Apache Software License, Version 1.1 * * Copyright (c) 2001-2003 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowledgment: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "The Jakarta Project", "Commons", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Software Foundation. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package org.apache.commons.collections; import java.util.HashMap; import java.util.Map; import junit.framework.TestCase; /** * * @author Neil O'Toole */ public class TestKeyValueHistory extends TestCase { private final String key = "key"; private final String value = "value"; private final String previous = "previous"; public TestKeyValueHistory(String testName) { super(testName); } public static void main(String[] args) { junit.textui.TestRunner.run(TestKeyValueHistory.class); } public void testKeyValueHistory() { // KVR with no previous value KeyValueHistory kvr = KeyValueHistory.createFromKeyValue(key, value); assertTrue( kvr.getKey() == key && kvr.getValue() == value && kvr.hasPreviousValues() == false); assertTrue(kvr.equals(kvr)); assertTrue(kvr.toString().equals("key=[value]")); // KVR with a previous value KeyValueHistory kvr2 = KeyValueHistory.createFromKeyValuePrevious(key, value, previous); assertTrue( kvr2.getKey() == key && kvr2.getValue() == value && kvr2.hasPreviousValues() == true && kvr2.getAllValues().get(1) == previous); assertTrue(kvr2.equals(kvr2)); assertTrue(kvr2.toString().equals("key=[value, previous]")); assertFalse(kvr.equals(kvr2)); assertFalse(kvr.hashCode() == kvr2.hashCode()); // test that a previous value of 'null' is treated differently to no previous value KeyValueHistory kvr3 = KeyValueHistory.createFromKeyValuePrevious(key, value, null); assertTrue(kvr3.equals(kvr3)); assertTrue(kvr3.toString().equals("key=[value, null]")); assertFalse(kvr3.equals(kvr)); assertFalse(kvr3.equals(kvr2)); assertFalse(kvr3.hashCode() == kvr2.hashCode()); assertFalse(kvr3.hashCode() == kvr.hashCode()); // test the Map.Entry handling Map map = new HashMap(); map.put(key, value); Map.Entry entry = (Map.Entry) map.entrySet().iterator().next(); KeyValueHistory kvr4 = KeyValueHistory.createFromMapEntry(entry); assertTrue( kvr4.getKey() == key && kvr4.getValue() == value && kvr4.hasPreviousValues() == false); assertTrue(kvr4.equals(kvr4)); assertTrue(kvr4.equals(kvr)); assertTrue(kvr4.toString().equals("key=[value]")); assertFalse("Map.Entry and KeyValueHistory are never equal", kvr4.equals(entry)); Map.Entry entry2 = kvr4.asMapEntry(); assertTrue(entry.equals(entry2)); assertTrue(entry.hashCode() == entry2.hashCode()); } }
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]
