Home | FAQ | Contact me

Tuple List

One day, I felt an urge to create this data structure, which is missing from Java.

Tuple.java:

This is the fundamental, cellular unit on which the tuple list is based.

package com.etretatlogiciels.collections;

import java.io.Serializable;

public class Tuple< K, V > implements Serializable
{
    K key;
    V value;

    public Tuple( K key, V value )
    {
        this.key   = key;
        this.value = value;
    }

    public Tuple( Tuple< K, V > tuple )
    {
        this.key   = tuple.getKey();
        this.value = tuple.getValue();
    }

    public K getKey()   { return this.key; }
    public V getValue() { return this.value; }

    /**
     * The importance of this method is to ensure that tuples, which are key-value
     * pairs, are examined for equality not by their object reference, but by their
     * content. Of course, for optimization sake, if the object references match,
     * then there's no need to compare the strings character by character.
     */
    public boolean equals( Object o )
    {
        if( !( o instanceof Tuple ) )
            return false;

        @SuppressWarnings( "unchecked" )
        Tuple< K, V > tuple = ( Tuple< K, V > ) o;
        K             key   = tuple.getKey();
        V             value = tuple.getValue();

        return ( ( key == this.key && value == this.value )
                || key.equals( this.key ) && value.equals( this.value ) );
    }
}
TupleList.java:

This is the implementation.

package com.etretatlogiciels.collections;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * This class sits atop Collection and uses ArrayList to create a collection of Tuples.
 * The motivation behind this is that we were using a Map to implement this, but it's
 * totally legitimate to have tuples with the same key, in fact, we were doing that
 * ourselves. That Map couldn't support this didn't really scream at us until testing
 * demonstrated it, a "Doh!" sort of moment.
 *
 * Therefore, this class is nothing more than a list of Tuples. There
 * may be duplicates, even duplicate identical by both their key and their value.
 *
 * @author Thomas Greger and Russell Bateman
 * @since August 2012
 */
public class TupleList extends XmlAdapter< Element, TupleList >
                         implements Collection< Tuple< String, String > >, Serializable
{
    private ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >();

    public TupleList() { }

    /**
     * This constructor names the field when serializing. Typically, it's called by a subclass and
     * the whole TupleList class is never used directly.
     * @param tagname
     */
    public TupleList( String tagname )
    {
        this.tagname = tagname;
    }

    /**
     * This is a special constructor created as shorthand for the JaxB serializer to use.
     * @param list array of tuples.
     */
    public TupleList( ArrayList< Tuple< String, String > > list )
    {
        addAll( list );
    }

    @Override
    public int size() { return this.list.size(); }

    @Override
    public boolean isEmpty() { return this.list.size() == 0; }

    @Override
    public boolean contains( Object o )
    {
        return list.contains( o );
    }

    /**
     * Determine whether the [ key, value ] pair are present in the list.
     *
     * @param key the key string.
     * @param value the value string.
     * @return true/false that the tuple is present.
     */
    public boolean contains( String key, String value )
    {
        for( Tuple< String, String > v : list )
        {
            if(  v.getKey().equals( key ) && v.getValue().equals( value ) )
              return true;
        }

        return false;
    }

    /**
     * Determine whether a tuple with the specified key is present in the list.
     *
     * @param key the key string.
     * @return true/false that the key is present.
     */
    public boolean contains( String key )
    {
        for( Tuple< String, String > v : list )
        {
            if(  v.getKey().equals( key ) )
              return true;
        }

        return false;
    }

    /**
     * Determine whether the [ key, value ] pair in the passed tuple are present
     * in the list.
     *
     * @param tuple containing the key and value to match.
     * @return true/false that the tuple is present.
     */
    public boolean contains( Tuple< String, String > tuple )
    {
        String key   = tuple.getKey();
        String value = tuple.getValue();

        return contains( key, value );
    }

    @Override
    public Iterator< Tuple< String, String > > iterator()
    {
        return list.iterator();
    }

    @Override
    public Object[] toArray()
    {
        return list.toArray();
    }

    @Override
    public < T > T[] toArray( T[] a )
    {
        return list.toArray( a );
    }

    /**
     * This is to overcome the difficulty I have working with the two preceding methods.
     * @return a copy of our array of tuples.
     */
    public ArrayList< Tuple< String, String > > toTupleArray()
    {
        ArrayList< Tuple< String, String > > array = new ArrayList< Tuple< String, String > >( this.size() );

        for( Tuple< String, String > tuple : list )
            array.add( new Tuple< String, String >( tuple ) );

        return array;
    }

    /**
     * Add a new tuple to the list.
     * @param [ key, value ] pair to add.
     * @return true/false that it was added.
     */
    @Override
    public boolean add( Tuple< String, String > e )
    {
        return list.add( e );
    }

    /**
     * Add a new tuple [ key, value ] pair to the list.
     * @param key the key string.
     * @param value the value string.
     * @return true/false that it was added.
     */
    public boolean add( String key, String value )
    {
        Tuple< String, String > tuple = new Tuple< String, String >( key, value );

        return add( tuple );
    }

    /**
     * Remove an existing tuple [ key, value ] pair to the list.
     * @param key the key string.
     * @param value the value string.
     * @return true/false that it was added.
     */
    @Override
    public boolean remove( Object o )
    {
        if( !( o instanceof Tuple ) )
            return false;

        @SuppressWarnings( "unchecked" )
        Tuple< String, String > t = ( Tuple< String, String > ) o;

        return list.remove( t );
    }

    /**
     * Remove an existing tuple [ key, value ] pair to the list.
     * @param key the key string.
     * @param value the value string.
     * @return true/false that it was added.
     */
    public boolean remove( String key, String value )
    {
        Tuple< String, String > tuple = new Tuple< String, String >( key, value );

        return list.remove( tuple );
    }

    @Override
    public boolean containsAll( Collection< ? > c )
    {
        return list.containsAll( c );
    }

    @Override
    public boolean addAll( Collection< ? extends Tuple< String, String > > c )
    {
        return list.addAll( c );
    }

    @Override
    public boolean removeAll( Collection< ? > c )
    {
        return list.removeAll( c );
    }

    @Override
    public boolean retainAll( Collection< ? > c )
    {
        return list.retainAll( c );
    }

    @Override
    public void clear()
    {
        list.clear();
    }

    public boolean equals( TupleList tuplelist )
    {
        if( tuplelist == this )
            return true;

        if( tuplelist.size() != this.size() )
            return false;

        // find every tuple in this object in the tuplelist passed or else!
        for( Tuple< String, String > tuple : list )
        {
            boolean found = false;

            ArrayList< Tuple< String, String > > thatlist = tuplelist.toTupleArray();

            for( Tuple< String, String > that : thatlist )
            {
                if( that.equals( tuple ) )
                    found = true;
            }

            if( !found )
                return false;
        }

        return true;
    }

    public String toString()
    {
        if( this.size() < 1 )
            return "";

        StringBuffer buffer = new StringBuffer( this.size() * 7 * 3 );

        buffer.append( "{<n" );

        int count = this.size();

        for( Tuple< String, String > tuple : list )
        {
            buffer.append( "  \"" + tuple.getKey() + "\"" );
            buffer.append( ":\""  + tuple.getValue() + "\"" );

            if( count -- > 1 )
                buffer.append( "," );

            buffer.append( "\n" );
        }

        buffer.append( "}" );

        return buffer.toString();
    }

    /**
     * Returns all tuples that with the indicated key. The point to this class in
     * the first place is that there can be any tuples including tuples with the same
     * key or even the same key and value.
     * @param key to search for.
     * @return list of matching tuples.
     */
    public ArrayList< Tuple< String, String > > getAll( String key )
    {
        ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >();

        for( Tuple< String, String > tuple : this.list )
        {
            if( tuple.key.equals( key ) )
                list.add( tuple );
        }

        return list;
    }

    /**
     * This returns the first tuple found with the specified key. There may be others,
     * but there is no order maintained so which value comes back cannot be predicted.
     * @param key to search for.
     * @return value associated with the key found.
     */
    public String getFirst( String key )
    {
        ArrayList< Tuple< String, String > > list = getAll( key );

        if( list == null || list.size() < 1 )
            return null;

        return list.get(  0 ).getValue();
    }

    /* ========== J A X B   s e r i a l i z a t i o n   c o d e ========================================
     * This enables us to make use of TupleList through Jersey. Originally, I got this code
     * for using Map/HashMap from Blaise Doughan, team lead for EclipseLink; see
     * http://stackoverflow.com/questions/11353790/serializer-for-hashmaps-for-jersey-use.
     */
    private static boolean  initDocumentBuilder = false;
    private DocumentBuilder builder = null;
    private String          tagname = "tuplelist";   // generic name; subclass this

    public final String getTagname() { return this.tagname; }

    private void initDocumentBuilder()
    {
        try
        {
            builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            initDocumentBuilder = true;
        }
        catch( Exception e )
        {
            e.printStackTrace();
        }
    }

    @Override
    public Element marshal( TupleList tuplelist ) throws Exception
    {
        if( !initDocumentBuilder )
            initDocumentBuilder();

        Document document    = builder.newDocument();
        Element  rootElement = document.createElement( tagname );

        document.appendChild( rootElement );

        ArrayList< Tuple< String, String > > list = tuplelist.toTupleArray();

        for( Tuple< String, String > tuple : list )
        {
            Element childElement = document.createElement( tuple.getKey() );

            childElement.setTextContent( tuple.getValue() );
            rootElement.appendChild( childElement );
        }

        return rootElement;
    }

    @Override
    public TupleList unmarshal( Element rootElement ) throws Exception
    {
        NodeList                             nodeList = rootElement.getChildNodes();
        ArrayList< Tuple< String, String > > list
                        = new ArrayList< Tuple< String, String > >( nodeList.getLength() );

        /* Pre-allocate the array we'll use in the TupleList; fill it using the child nodes
         * of the in-coming Element.
         */
        for( int x = 0; x < nodeList.getLength(); x++ )
        {
            Node node = nodeList.item( x );

            if( node.getNodeType() == Node.ELEMENT_NODE )
                list.add( new Tuple< String, String >( node.getNodeName(), node.getTextContent() ) );
        }

        this.list = list;
        return this;
    }
}
TupleListTest.java:

...and a test for it.

package com.etretatlogiciels.collections;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;
import java.util.ArrayList;
import java.util.Iterator;

import org.junit.Test;

public class TupleListTest
{
    private static final String SMACK_NAME = "Smackaroonie";
    private static final String SHIZL_NAME = "Schizzle";
    private static final String SHICK_NAME = "Schnikies";

    private static TupleList SMACK = new TupleList( "smackaroonie" );
    private static TupleList SHICK = new TupleList( "smackaroonie" );

    static
    {
        SMACK.add( "companyname", SMACK_NAME );
        SMACK.add( SHIZL_NAME,    "write" );
        SMACK.add( SHIZL_NAME,    "password" );
        SMACK.add( SHICK_NAME,    "read" );
        SMACK.add( SHICK_NAME,    "password" );

        SHICK.add( SHICK_NAME,    "Schnikies" );
        SHICK.add( SMACK_NAME,    "read" );
        SHICK.add( SMACK_NAME,    "password" );
    }

    @Test
    public void testTupleList_arrayconstructor()
    {
        ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >();

        list.add( new Tuple< String, String >( "companyname", SMACK_NAME ) );
        list.add( new Tuple< String, String >( SHIZL_NAME,    "write" ) );
        list.add( new Tuple< String, String >( SHIZL_NAME,    "password" ) );
        list.add( new Tuple< String, String >( SHICK_NAME,    "read" ) );
        list.add( new Tuple< String, String >( SHICK_NAME,    "password" ) );

        TupleList s = new TupleList( list );

        assertTrue( SMACK.containsAll( s.toTupleArray() ) );
    }

    @Test
    public void testTupleList_tagnameconstructor()
    {
        TupleList s = new TupleList( "smackaroonie" );

        assertTrue( s.getTagname().equals( "smackaroonie" ) );
    }

    @Test
    public void testSize()
    {
        assertEquals( SMACK.size(), 5 );
    }

    @Test
    public void testIsEmpty()
    {
        TupleList s = new TupleList();

        assertTrue( "This one is not empty", SMACK.size() != 0 );
        assertFalse( "This one is empty", s.size() != 0 );
    }

    @Test
    public void testContains_object()
    {
        Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME );
        assertTrue( SMACK.contains( ( Object ) tuple ) );
    }

    @Test
    public void testContains_keyvaluepair()
    {
        assertTrue( SMACK.contains( "companyname", SMACK_NAME ) );
    }

    @Test
    public void testContains_key()
    {
        assertTrue( SMACK.contains( "companyname" ) );
    }

    @Test
    public void testContains_tuple()
    {
        Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME );
        assertTrue( SMACK.contains( tuple ) );
    }

    @Test
    public void testIterator()
    {
        Iterator< Tuple< String, String > > iterator = SMACK.iterator();

        assertNotNull( iterator );

        while( iterator.hasNext() )
        {
            Tuple< String, String > tuple = iterator.next();

            assertNotNull( tuple );
        }
    }

    @Test
    public void testToArray()
    {
        Object[] object = SMACK.toArray();

        assertTrue( object.length == SMACK.size() );
    }

    @Test
    public void testToArray_tarray()
    {
        @SuppressWarnings( "unchecked" )
        Tuple< String, String >[] list = new Tuple[ SMACK.size() ];

        list = SMACK.toArray( list );

        int length = list.length;

        assertTrue( length == SMACK.size() );

        TupleList s = new TupleList( SMACK.toTupleArray() );

        ArrayList< Tuple< String, String > > tuples = new ArrayList< Tuple< String, String > >( length );

        for( int i = 0; i < length; i++ )
            tuples.add( new Tuple< String, String >( list[ i ].key, list[ i ].value ) );

        assertTrue( s.removeAll( tuples ) );
        assertTrue( s.size() == 0 );
    }

    @Test
    public void testToTupleArray()
    {
        ArrayList< Tuple< String, String > > list = SMACK.toTupleArray();

        assertTrue( list.size() == SMACK.size() );
    }

    @Test
    public void testAdd_tuple()
    {
        Tuple< String, String > tuple = new Tuple< String, String >( "this", "and that" );
        TupleList             s     = new TupleList();

        assertTrue( s.add( tuple ) );
        assertTrue( s.size() == 1 );
    }

    @Test
    public void testAdd_keyvaluepair()
    {
        TupleList s = new TupleList();

        assertTrue( s.add( "this", "and that" ) );
        assertTrue( s.size() == 1 );
    }

    @Test
    public void testRemove_object()
    {
        Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME );
        TupleList             s     = new TupleList( SMACK.toTupleArray() );

        s.remove( ( Object ) tuple );

        assertTrue( s.size() == SMACK.size() - 1 );
    }

    @Test
    public void testRemove_keyvaluepair()
    {
        TupleList s = new TupleList( SMACK.toTupleArray() );

        s.remove( "companyname", SMACK_NAME );

        assertTrue( s.size() == SMACK.size() - 1 );
    }

    @Test
    public void testContainsAll()
    {
        TupleList s = new TupleList( SMACK.toTupleArray() );
        assertTrue( SMACK.containsAll( s.toTupleArray() ) );
    }

    @Test
    public void testAddAll()
    {
        TupleList s = new TupleList( SMACK.toTupleArray() );

        assertTrue( s.addAll( SHICK.toTupleArray() ) );
    }

    @Test
    public void testRemoveAll()
    {
        TupleList s = new TupleList( SMACK.toTupleArray() );

        s.removeAll( SMACK.toTupleArray() );

        assertTrue( s.isEmpty() );
    }

    @Test
    public void testRetainAll()
    {
        Tuple< String, String >              tuple = new Tuple< String, String >( "companyname", SMACK_NAME );
        ArrayList< Tuple< String, String > > list  = SMACK.toTupleArray();

        // remove one of the tuples...
        list.remove( tuple );

        TupleList s = new TupleList( SMACK.toTupleArray() );

        // retain from the origin SMACK list only the tuples present in the list (from which we removed one)...
        s.retainAll( list );

        assertTrue( SMACK.size() - 1 == s.size() );
    }

    @Test
    public void testClear()
    {
        TupleList s = new TupleList( SMACK.toTupleArray() );

        s.clear();

        assertTrue( s.size() == 0 );
    }

    @Test
    public void testEquals()
    {
        TupleList s = new TupleList( SMACK.toTupleArray() );

        assertTrue( s.equals( SMACK ) );
    }

    @Test
    public void testToString()
    {
        System.out.println( SMACK.toString() );
    }

    @Test
    public void testGetAll()
    {
        TupleList                          s    = new TupleList( SMACK.toTupleArray() );
        ArrayList< Tuple< String, String > > list = s.getAll( SHICK_NAME );

        int length = s.size();

        assertTrue( list.size() == 2 );         // should have found 2 tuples...
        assertTrue( s.removeAll( list ) );      // should modify the internal list...
        assertTrue( s.size() == length - 2 );   // should have matched (and removed) the 2 tuples...
    }

    @Test
    public void testGetFirst()
    {
        String value = SMACK.getFirst( "companyname" );

        assertNotNull( value );
        assertTrue( value.equals( SMACK_NAME ) );
    }
}