/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002-2006, GeoTools Project Managment Committee (PMC)
 * 
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data.mysql;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.NoSuchElementException;
import java.util.PropertyResourceBundle;
import java.util.logging.Logger;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.geotools.data.DataTestCase;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultQuery;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.EmptyFeatureReader;
import org.geotools.data.FeatureLock;
import org.geotools.data.FeatureLockFactory;
import org.geotools.data.FeatureLocking;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.FeatureWriter;
import org.geotools.data.FilteringFeatureReader;
import org.geotools.data.InProcessLockingManager;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.data.jdbc.datasource.ManageableDataSource;
import org.geotools.data.jdbc.fidmapper.BasicFIDMapper;
import org.geotools.data.jdbc.fidmapper.TypedFIDMapper;
import org.geotools.feature.AttributeType;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.FeatureType;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.SimpleFeature;
import org.geotools.filter.AbstractFilter;
import org.geotools.filter.CompareFilter;
import org.geotools.filter.Expression;
import org.opengis.filter.Filter;
import org.geotools.filter.FilterFactory;
import org.geotools.filter.FilterFactoryFinder;

import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;

/**
 * This class tests the MySQLDataStore API against the same tests as
 * MemoryDataStore.
 * 
 * <p>
 * The test fixture is available in the shared DataTestCase, really the common
 * elements should move to a shared DataStoreAPITestCase.
 * </p>
 * 
 * <p>
 * This class does require your own DataStore, it will create a table populated
 * with the Features from the test fixture, and run a test, and then remove
 * the table.
 * </p>
 * 
 * <p>
 * Because of the nature of this testing process you cannot run these tests in
 * conjunction with another user, so they cannot be implemented against the
 * public server.
 * </p>
 * 
 * <p>
 * A simple properties file has been constructed,
 * <code>fixture.properties</code>, which you may direct to your own potgis
 * database installation.
 * </p>
 *
 * @author Jody Garnett, Refractions Research
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/mysql/src/test/java/org/geotools/data/mysql/MySQLDataStoreAPITest.java $
 */
public class MySQLDataStoreAPITest extends DataTestCase {
    private static final int LOCK_DURATION = 3600 * 1000;  // one hour
    /** The logger for the filter module. */
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.mysql");
    static boolean CHECK_TYPE = false;
    MySQLDataStore data;
    ManageableDataSource pool;
    String database;
    String victim = null; //"testGetFeatureWriterRemoveAll";

    /**
     * Constructor for MemoryDataStoreTest.
     *
     * @param test
     *
     * @throws AssertionError DOCUMENT ME!
     */
    public MySQLDataStoreAPITest(String test) {
        super(test);

        if ((victim != null) && !test.equals(victim)) {
            throw new AssertionError("test supressed " + test);
        }
    }
    
    public static void main(String[] args) {
        junit.textui.TestRunner.run(suite());
    }
    
    public static Test suite() {
        LOGGER.info("starting suite...");

        TestSuite suite = new TestSuite(MySQLDataStoreAPITest.class);
        LOGGER.info("made suite...");

        return suite;
    }    

    /**
     * @see TestCase#setUp()
     */
    protected void setUp() throws Exception {
        super.setUp();

        PropertyResourceBundle resource;
        resource =
            new PropertyResourceBundle(this.getClass().getResourceAsStream("fixture.properties"));

        String namespace = resource.getString("namespace");
        String host = resource.getString("host");
        int port = Integer.parseInt(resource.getString("port"));
        String database = resource.getString("database");
        this.database = database;

        String user = resource.getString("user");
        String password = resource.getString("passwd");

        if (namespace.equals("http://www.geotools.org/data/postgis")) {
            throw new IllegalStateException(
                "The fixture.properties file needs to be configured for your own database");
        }
        
        pool = MySQLDataStoreFactory.getDefaultDataSource(host, user, password, port, database, 10, 2, false);


        setUpRoadTable();
        setUpRiverTable();

        if (CHECK_TYPE) {
            checkTypesInDataBase();
            CHECK_TYPE = false; // just once
        }

        data = new MySQLDataStore(pool, null, getName());
        data.setFIDMapper("road", new TypedFIDMapper(new BasicFIDMapper("fid", 255, false), "road"));
        data.setFIDMapper("river", new TypedFIDMapper(new BasicFIDMapper("fid", 255, false), "river"));
        data.setFIDMapper("testset", new TypedFIDMapper(new BasicFIDMapper("gid", 255, true), "testset"));

        //
        // Update Fixture to reflect the actual data in the database
        // I am doing this because it
        updateRoadFeaturesFixture();
        updateRiverFeaturesFixture();
    }

    /**
     * This is a quick hack to have our fixture reflect the FIDs in the
     * database.
     * 
     * <p>
     * When the dataStore learns how to preserve our FeatureIds this won't be
     * required.
     * </p>
     *
     * @throws Exception
     */
    protected void updateRoadFeaturesFixture() throws Exception {
        Connection conn = pool.getConnection();
        FeatureReader reader =
            data.getFeatureReader(new DefaultQuery("road", Filter.INCLUDE), Transaction.AUTO_COMMIT);

        Envelope bounds = new Envelope();

        try {
            SimpleFeature f;

            while (reader.hasNext()) {
                f = (SimpleFeature)reader.next();

                int index = ((Integer) f.getAttribute("id")).intValue() - 1;
                roadFeatures[index] = f;
                bounds.expandToInclude(f.getBounds());
            }
        } finally {
            reader.close();
            conn.close();
        }

        if (!roadBounds.equals(bounds)) {
            System.out.println("warning! Database changed bounds()");
            System.out.println("was:" + roadBounds);
            System.out.println("now:" + bounds);
            roadBounds = bounds;
        }

        Envelope bounds12 = new Envelope();
        bounds12.expandToInclude(roadFeatures[0].getBounds());
        bounds12.expandToInclude(roadFeatures[1].getBounds());

        if (!rd12Bounds.equals(bounds12)) {
            System.out.println("warning! Database changed bounds of rd1 & rd2");
            System.out.println("was:" + rd12Bounds);
            System.out.println("now:" + bounds12);
            rd12Bounds = bounds12;
        }

        FeatureType schema = roadFeatures[0].getFeatureType();
        FilterFactory ff = FilterFactoryFinder.createFilterFactory();
        CompareFilter tFilter = ff.createCompareFilter(AbstractFilter.COMPARE_EQUALS);
        Expression rd1Literal = ff.createLiteralExpression("r1");
        tFilter.addLeftValue(rd1Literal);

        Expression rdNameAtt = ff.createAttributeExpression(schema, "name");
        tFilter.addRightValue(rdNameAtt);
        rd1Filter = tFilter;

        tFilter = ff.createCompareFilter(AbstractFilter.COMPARE_EQUALS);

        Expression rd2Literal = ff.createLiteralExpression("r2");
        tFilter.addLeftValue(rd2Literal);
        tFilter.addRightValue(rdNameAtt);
        rd2Filter = tFilter;
        rd12Filter = ff.or( rd2Filter, rd1Filter);
    }

    /**
     * This is a quick hack to have our fixture reflect the FIDs in the
     * database.
     * 
     * <p>
     * When the dataStore learns how to preserve our FeatureIds this won't be
     * required.
     * </p>
     *
     * @throws Exception DOCUMENT ME!
     */
    protected void updateRiverFeaturesFixture() throws Exception {
        Connection conn = pool.getConnection();
        FeatureReader reader =
            data.getFeatureReader(new DefaultQuery("river", Filter.INCLUDE), Transaction.AUTO_COMMIT);

        Envelope bounds = new Envelope();

        try {
            Feature f;

            while (reader.hasNext()) {
                f = reader.next();

                int index = ((Integer) f.getAttribute("id")).intValue() - 1;
                riverFeatures[index] = f;
                bounds.expandToInclude(f.getBounds());
            }
        } finally {
            reader.close();
            conn.close();
        }

        if (!riverBounds.equals(bounds)) {
            System.out.println("warning! Database changed bounds of river");
            System.out.println("was:" + riverBounds);
            System.out.println("now:" + bounds);
            riverBounds = bounds;
        }

        FilterFactory factory = FilterFactoryFinder.createFilterFactory();
        CompareFilter tFilter = factory.createCompareFilter(AbstractFilter.COMPARE_EQUALS);
        Expression rvLiteral = factory.createLiteralExpression("rv1");
        tFilter.addLeftValue(rvLiteral);

        FeatureType schema = riverFeatures[0].getFeatureType();
        Expression rvNameAtt = factory.createAttributeExpression(schema, "river");
        tFilter.addRightValue(rvNameAtt);
        rv1Filter = tFilter;
    }

    protected void checkTypesInDataBase() throws SQLException {
        Connection conn = pool.getConnection();

        try {
            DatabaseMetaData md = conn.getMetaData();
            ResultSet rs =
                //md.getTables( catalog, null, null, null );
    md.getTables(null, "public", "%", new String[] { "TABLE", });
            ResultSetMetaData rsmd = rs.getMetaData();
            int NUM = rsmd.getColumnCount();
            System.out.print(" ");

            for (int i = 1; i <= NUM; i++) {
                System.out.print(rsmd.getColumnName(i));
                System.out.flush();
                System.out.print(":");
                System.out.flush();
                System.out.print(rsmd.getColumnClassName(i));
                System.out.flush();

                if (i < NUM) {
                    System.out.print(",");
                    System.out.flush();
                }
            }

            System.out.println();

            while (rs.next()) {
                System.out.print(rs.getRow());
                System.out.print(":");
                System.out.flush();

                for (int i = 1; i <= NUM; i++) {
                    System.out.print(rsmd.getColumnName(i));
                    System.out.flush();
                    System.out.print("=");
                    System.out.flush();
                    System.out.print(rs.getString(i));
                    System.out.flush();

                    if (i < NUM) {
                        System.out.print(",");
                        System.out.flush();
                    }
                }

                System.out.println();
            }
        } finally {
            conn.close();
        }
    }

    protected void setUpRoadTable() throws Exception {
        Connection conn = pool.getConnection();
        conn.setAutoCommit(true);

        try {
            Statement s = conn.createStatement();
            s.execute("DROP TABLE road");
        } catch (Exception ignore) {
        }

        try {
            Statement s = conn.createStatement();

            s.execute(
                "CREATE TABLE road (fid varchar(255) PRIMARY KEY, id int, geom LINESTRING not null, name varchar(255), spatial index(geom) ) engine = myisam");

            for (int i = 0; i < roadFeatures.length; i++) {
                Feature f = roadFeatures[i];

                //strip out the road. 
                String fid = f.getID().substring("road.".length());
                String ql =
                    "INSERT INTO road (fid,id,geom,name) VALUES ("
                        + "'"
                        + fid
                        + "',"
                        + f.getAttribute("id")
                        + ","
                        + "GeometryFromText('"
                        + ((Geometry) f.getAttribute("geom")).toText()
                        + "', 0 ),"
                        + "'"
                        + f.getAttribute("name")
                        + "')";

                s.execute(ql);
            }
        } finally {
            conn.close();
        }
    }

    protected void killTestTables() throws Exception {
        Connection conn = pool.getConnection();

        try {
            Statement s = conn.createStatement();
            s.execute("SELECT dropgeometrycolumn( '" + database + "','road','geom')");
            s.execute("SELECT dropgeometrycolumn( '" + database + "','river','geom')");
        } catch (Exception ignore) {
        }

        try {
            Statement s = conn.createStatement();
            s.execute("DROP TABLE road");
            s.execute("DROP TABLE river");
        } catch (Exception ignore) {
        } finally {
            conn.close();
        }
    }

    protected void setUpRiverTable() throws Exception {
        Connection conn = pool.getConnection();

        try {
            Statement s = conn.createStatement();
            s.execute("DROP TABLE river");
        } catch (Exception ignore) {
        }

        try {
            Statement s = conn.createStatement();

            //postgis = new PostgisDataSource(connection, FEATURE_TABLE);
            s.execute("CREATE TABLE river(fid varchar(255) PRIMARY KEY, id int, geom MULTILINESTRING, river varchar(255), flow double)");

            for (int i = 0; i < riverFeatures.length; i++) {
                Feature f = riverFeatures[i];
                String fid = f.getID().substring("river.".length());
                s.execute(
                    "INSERT INTO river (fid, id, geom, river, flow) VALUES ("
                        + "'"
                        + fid
                        + "',"
                        + f.getAttribute("id")
                        + ","
                        + "GeometryFromText('"
                        + f.getAttribute("geom").toString()
                        + "', 0 ),"
                        + "'"
                        + f.getAttribute("river")
                        + "',"
                        + f.getAttribute("flow")
                        + ")");
            }
        } finally {
            conn.close();
        }
    }

    /*
     * @see TestCase#tearDown()
     */
    protected void tearDown() throws Exception {
        data.dispose();
        data = null;
        
        super.tearDown();
    }

    public void testGetFeatureTypes() {
        try {
            String[] names = data.getTypeNames();
            assertTrue(contains(names, "road"));
            assertTrue(contains(names, "river"));
        } catch (IOException e) {
            e.printStackTrace();
            fail("An IOException has been thrown!");
        }
    }

    boolean contains(Object[] array, Object expected) {
        if ((array == null) || (array.length == 0)) {
            return false;
        }

        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(expected)) {
                return true;
            }
        }

        return false;
    }

    void assertContains(Object[] array, Object expected) {
        assertFalse(array == null);
        assertFalse(array.length == 0);
        assertNotNull(expected);

        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(expected)) {
                return;
            }
        }

        fail("Contains " + expected);
    }

    /**
     * Like contain but based on match rather than equals
     *
     * @param array DOCUMENT ME!
     * @param expected DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    boolean containsLax(Feature[] array, Feature expected) {
        if ((array == null) || (array.length == 0)) {
            return false;
        }

        FeatureType type = expected.getFeatureType();

        for (int i = 0; i < array.length; i++) {
            if (match(array[i], expected)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Compare based on attributes not getID allows comparison of Diff contents
     *
     * @param expected DOCUMENT ME!
     * @param actual DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    boolean match(Feature expected, Feature actual) {
        FeatureType type = expected.getFeatureType();

        for (int i = 0; i < type.getAttributeCount(); i++) {
            Object av = actual.getAttribute(i);
            Object ev = expected.getAttribute(i);

            if ((av == null) && (ev != null)) {
                return false;
            } else if ((ev == null) && (av != null)) {
                return false;
            } else if (av instanceof Geometry && ev instanceof Geometry) {
                Geometry ag = (Geometry) av;
                Geometry eg = (Geometry) ev;

                if (!ag.equals(eg)) {
                    return false;
                }
            } else if (!av.equals(ev)) {
                return false;
            }
        }

        return true;
    }

    public void testGetSchemaRoad() throws IOException {
        FeatureType expected = roadType;
        FeatureType actual = data.getSchema("road");
        assertEquals("namespace", expected.getNamespace(), actual.getNamespace());
        assertEquals("typeName", expected.getTypeName(), actual.getTypeName());

        //assertEquals( "compare", 0, DataUtilities.compare( expected, actual ));
        assertEquals("attributeCount", expected.getAttributeCount(), actual.getAttributeCount());

        for (int i = 0; i < expected.getAttributeCount(); i++) {
            AttributeType expectedAttribute = expected.getAttributeType(i);
            AttributeType actualAttribute = actual.getAttributeType(i);
            assertEquals(
                "attribute " + expectedAttribute.getName(),
                expectedAttribute,
                actualAttribute);
        }

        assertEquals(expected, actual);
    }

    public void testGetSchemaRiver() throws IOException {
        FeatureType expected = riverType;
        FeatureType actual = data.getSchema("river");
        assertEquals("namespace", expected.getNamespace(), actual.getNamespace());
        assertEquals("typeName", expected.getTypeName(), actual.getTypeName());

        //assertEquals( "compare", 0, DataUtilities.compare( expected, actual ));
        assertEquals("attributeCount", expected.getAttributeCount(), actual.getAttributeCount());

        for (int i = 0; i < expected.getAttributeCount(); i++) {
            AttributeType expectedAttribute = expected.getAttributeType(i);
            AttributeType actualAttribute = actual.getAttributeType(i);
            assertEquals(
                "attribute " + expectedAttribute.getName(),
                expectedAttribute,
                actualAttribute);
        }

        assertEquals(expected, actual);
    }

    static public void assertEquals(String message, String expected, String actual) {
        if (expected == actual) {
            return;
        }

        assertNotNull(message, expected);
        assertNotNull(message, actual);

        if (!expected.equals(actual)) {
            fail(message + " expected:<" + expected + "> but was <" + actual + ">");
        }
    }

    void assertCovers(String msg, FeatureCollection c1, FeatureCollection c2) {
        if (c1 == c2) {
            return;
        }

        assertNotNull(msg, c1);
        assertNotNull(msg, c2);
        assertEquals(msg + " size", c1.size(), c2.size());

        Feature f;

        for (FeatureIterator i = c1.features(); i.hasNext();) {
            f = i.next();
            assertTrue(msg + " " + f.getID(), c2.contains(f));
        }
    }

    public FeatureReader reader(String typeName) throws IOException {
        FeatureType type = data.getSchema(typeName);

        return data.getFeatureReader(type, Filter.INCLUDE, Transaction.AUTO_COMMIT);
    }

    public FeatureWriter writer(String typeName) throws IOException {
        return data.getFeatureWriter(typeName, Transaction.AUTO_COMMIT);
    }

    public void testGetFeatureReader() throws IOException, IllegalAttributeException {
        assertCovered(roadFeatures, reader("road"));
        assertEquals(3, count(reader("road")));
    }

    public void testGetFeatureReaderMutability() throws IOException, IllegalAttributeException {
        FeatureReader reader = reader("road");
        Feature feature;

        while (reader.hasNext()) {
            feature = (Feature) reader.next();
            feature.setAttribute("name", null);
        }

        reader.close();

        reader = reader("road");

        while (reader.hasNext()) {
            feature = (Feature) reader.next();
            assertNotNull(feature.getAttribute("name"));
        }

        reader.close();

        try {
            reader.next();
            fail("next should fail with an IOException");
        } catch (IOException expected) {
        }
    }

    public void testGetFeatureReaderConcurancy()
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureReader reader1 = reader("road");
        FeatureReader reader2 = reader("road");
        FeatureReader reader3 = reader("river");

        Feature feature1;
        Feature feature2;
        Feature feature3;

        while (reader1.hasNext() || reader2.hasNext() || reader3.hasNext()) {
            assertTrue(contains(roadFeatures, reader1.next()));
            assertTrue(contains(roadFeatures, reader2.next()));

            if (reader3.hasNext()) {
                assertTrue(contains(riverFeatures, reader3.next()));
            }
        }

        try {
            reader1.next();
            fail("next should fail with an NoSuchElementException");
        } catch (NoSuchElementException expectedNoElement) {
            // this is new to me, I had expected an IOException
        }

        try {
            reader2.next();
            fail("next should fail with an NoSuchElementException");
        } catch (NoSuchElementException expectedNoElement) {
        }

        try {
            reader3.next();
            fail("next should fail with an NoSuchElementException");
        } catch (NoSuchElementException expectedNoElement) {
        }

        reader1.close();
        reader2.close();
        reader3.close();

        try {
            reader1.next();
            fail("next should fail with an IOException");
        } catch (IOException expectedClosed) {
        }

        try {
            reader2.next();
            fail("next should fail with an IOException");
        } catch (IOException expectedClosed) {
        }

        try {
            reader3.next();
            fail("next should fail with an IOException");
        } catch (IOException expectedClosed) {
        }
    }

    public void testGetFeatureReaderFilterAutoCommit()
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureType type = data.getSchema("road");
        FeatureReader reader;

        reader = data.getFeatureReader(type, Filter.INCLUDE, Transaction.AUTO_COMMIT);
        assertFalse(reader instanceof FilteringFeatureReader);
        assertEquals(type, reader.getFeatureType());
        assertEquals(roadFeatures.length, count(reader));

        reader = data.getFeatureReader(type, Filter.EXCLUDE, Transaction.AUTO_COMMIT);
        assertTrue(reader instanceof EmptyFeatureReader);

        assertEquals(type, reader.getFeatureType());
        assertEquals(0, count(reader));

        reader = data.getFeatureReader(type, rd1Filter, Transaction.AUTO_COMMIT);

        //assertTrue(reader instanceof FilteringFeatureReader);
        assertEquals(type, reader.getFeatureType());
        assertEquals(1, count(reader));
    }

// TODO: uncomment when and if MySQLDataStore gets transaction capabilities
//    public void testGetFeatureReaderFilterTransaction()
//        throws NoSuchElementException, IOException, IllegalAttributeException {
//        Transaction t = new DefaultTransaction();
//        FeatureType type = data.getSchema("road");
//        FeatureReader reader;
//
//        reader = data.getFeatureReader(type, Filter.EXCLUDE, t);
//        assertTrue(reader instanceof EmptyFeatureReader);
//        assertEquals(type, reader.getFeatureType());
//        assertEquals(0, count(reader));
//
//        reader = data.getFeatureReader(type, Filter.INCLUDE, t);
//        assertEquals(type, reader.getFeatureType());
//        assertEquals(roadFeatures.length, count(reader));
//
//        reader = data.getFeatureReader(type, rd1Filter, t);
//        assertEquals(type, reader.getFeatureType());
//        assertEquals(1, count(reader));
//
//        FeatureWriter writer = data.getFeatureWriter("road", Filter.INCLUDE, t);
//        Feature feature;
//
//        while (writer.hasNext()) {
//            feature = writer.next();
//
//            if (feature.getID().equals(roadFeatures[0].getID())) {
//                writer.remove();
//            }
//        }
//
//        reader = data.getFeatureReader(type, Filter.EXCLUDE, t);
//        assertEquals(0, count(reader));
//
//        reader = data.getFeatureReader(type, Filter.INCLUDE, t);
//        assertEquals(roadFeatures.length - 1, count(reader));
//
//        reader = data.getFeatureReader(type, rd1Filter, t);
//        assertEquals(0, count(reader));
//
//        t.rollback();
//        reader = data.getFeatureReader(type, Filter.EXCLUDE, t);
//        assertEquals(0, count(reader));
//
//        reader = data.getFeatureReader(type, Filter.INCLUDE, t);
//        assertEquals(roadFeatures.length, count(reader));
//
//        reader = data.getFeatureReader(type, rd1Filter, t);
//        assertEquals(1, count(reader));
//    }

    /**
     * Ensure readers contents equal those in the feature array
     *
     * @param features DOCUMENT ME!
     * @param reader DOCUMENT ME!
     *
     * @throws NoSuchElementException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     * @throws IllegalAttributeException DOCUMENT ME!
     */
    void assertCovered(Feature[] features, FeatureReader reader)
        throws NoSuchElementException, IOException, IllegalAttributeException {
        int count = 0;

        try {
            while (reader.hasNext()) {
                assertContains(features, reader.next());
                count++;
            }
        } finally {
            reader.close();
        }

        assertEquals(features.length, count);
    }

    /**
     * Ensure readers contents match those in the feature array
     * 
     * <p>
     * Implemented using match on attribute types, not feature id
     * </p>
     *
     * @param array DOCUMENT ME!
     * @param reader DOCUMENT ME!
     *
     * @throws Exception DOCUMENT ME!
     */
    void assertMatched(Feature[] array, FeatureReader reader) throws Exception {
        Feature feature;
        int count = 0;

        try {
            while (reader.hasNext()) {
                feature = reader.next();
                assertMatch(array, feature);
                count++;
            }
        } finally {
            reader.close();
        }

        assertEquals("array not matched by reader", array.length, count);
    }

    void assertMatch(Feature[] array, Feature feature) {
        assertTrue(array != null);
        assertTrue(array.length != 0);

        FeatureType schema = feature.getFeatureType();

        for (int i = 0; i < array.length; i++) {
            if (match(array[i], feature)) {
                return;
            }
        }

        System.out.println("not found:" + feature);

        for (int i = 0; i < array.length; i++) {
            System.out.println(i + ":" + array[i]);
        }

        fail("array has no match for " + feature);
    }

    /**
     * Ensure that FeatureReader reader contains extactly the contents of
     * array.
     *
     * @param reader DOCUMENT ME!
     * @param array DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     *
     * @throws NoSuchElementException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     * @throws IllegalAttributeException DOCUMENT ME!
     */
    boolean covers(FeatureReader reader, Feature[] array)
        throws NoSuchElementException, IOException, IllegalAttributeException {
        Feature feature;
        int count = 0;

        try {
            while (reader.hasNext()) {
                feature = reader.next();

                if (!contains(array, feature)) {
                    return false;
                }

                count++;
            }
        } finally {
            reader.close();
        }

        return count == array.length;
    }
    boolean covers(FeatureIterator reader, Feature[] array)
    throws NoSuchElementException, IOException, IllegalAttributeException {
    Feature feature;
    int count = 0;

    try {
        while (reader.hasNext()) {
            feature = reader.next();

            if (!contains(array, feature)) {
                return false;
            }

            count++;
        }
    } finally {
        reader.close();
    }

    return count == array.length;
}

    boolean coversLax(FeatureReader reader, Feature[] array)
        throws NoSuchElementException, IOException, IllegalAttributeException {
        Feature feature;
        int count = 0;

        try {
            while (reader.hasNext()) {
                feature = reader.next();

                if (!containsLax(array, feature)) {
                    return false;
                }

                count++;
            }
        } finally {
            reader.close();
        }

        return count == array.length;
    }

    void dump(String message, FeatureReader reader)
        throws NoSuchElementException, IOException, IllegalAttributeException {
        Feature feature;
        int count = 0;

        try {
            while (reader.hasNext()) {
                feature = reader.next();

                String msg = message + ": feture " + count + "=" + feature;

                //LOGGER.info( msg );
                System.out.println(msg);
                count++;
            }
        } finally {
            reader.close();
        }
    }

    void dump(String message, Object[] array) {
        for (int i = 0; i < array.length; i++) {
            String msg = message + ": " + i + "=" + array[i];

            //LOGGER.info( msg );
            System.out.println(msg);
        }
    }

    /*
     * Test for FeatureWriter getFeatureWriter(String, Filter, Transaction)
     */
    public void testGetFeatureWriter() throws Exception {
        FeatureWriter writer = data.getFeatureWriter("road", Filter.INCLUDE, Transaction.AUTO_COMMIT);
        assertEquals(roadFeatures.length, count(writer));
    }

    public void testGetFeatureWriterClose() throws Exception {
        FeatureWriter writer = data.getFeatureWriter("road", Filter.INCLUDE, Transaction.AUTO_COMMIT);

        writer.close();

        try {
            assertFalse(writer.hasNext());
            fail("Should not be able to use a closed writer");
        } catch (IOException expected) {
        }

        try {
            assertNull(writer.next());
            fail("Should not be able to use a closed writer");
        } catch (IOException expected) {
        }

        try {
            writer.close();
        } catch (IOException expected) {
        }
    }

    public void testGetFeatureWriterRemove() throws IOException, IllegalAttributeException {
        FeatureWriter writer = writer("road");
        Feature feature;

        while (writer.hasNext()) {
            feature = writer.next();

            if (feature.getID().equals(roadFeatures[0].getID())) {
                writer.remove();
            }
        }

        assertEquals(roadFeatures.length - 1, count("road"));
    }
    public void testGetFeatureWriterRemoveAll() throws IOException, IllegalAttributeException {
        FeatureWriter writer = writer("road");
        Feature feature;

        try {
            while (writer.hasNext()) {
                feature = writer.next();
                writer.remove();
            }
        } finally {
            writer.close();
        }
        assertEquals(0, count("road"));
    }

    public int count(String typeName) throws IOException {
        //return count(reader(typeName));
        // makes use of optimization if any
        return data.getFeatureSource(typeName).getFeatures().size();
    }

    public void testGetFeaturesWriterAdd() throws IOException, IllegalAttributeException {
        FeatureWriter writer = data.getFeatureWriter("road", Transaction.AUTO_COMMIT);
        SimpleFeature feature;

        LOGGER.info("about to call has next on writer " + writer);

        while (writer.hasNext()) {
            feature = (SimpleFeature)writer.next();
        }

        assertFalse(writer.hasNext());

        feature = (SimpleFeature)writer.next();
        feature.setAttributes(newRoad.getAttributes(null));
        writer.write();

        assertFalse(writer.hasNext());
        assertEquals(roadFeatures.length + 1, count("road"));
    }

    /**
     * Seach for feature based on AttributeType.
     * 
     * <p>
     * If attributeName is null, we will search by feature.getID()
     * </p>
     * 
     * <p>
     * The provided reader will be closed by this opperations.
     * </p>
     *
     * @param reader reader to search through
     * @param attributeName attributeName, or null for featureID
     * @param value value to match
     *
     * @return Feature
     *
     * @throws NoSuchElementException if a match could not be found
     * @throws IOException We could not use reader
     * @throws IllegalAttributeException if attributeName did not match schema
     */
    public Feature findFeature(FeatureReader reader, String attributeName, Object value)
        throws NoSuchElementException, IOException, IllegalAttributeException {
        Feature f;

        try {
            while (reader.hasNext()) {
                f = reader.next();

                if (attributeName == null) {
                    if (value.equals(f.getID())) {
                        return f;
                    }
                } else {
                    if (value.equals(f.getAttribute(attributeName))) {
                        return f;
                    }
                }
            }
        } finally {
            reader.close();
        }

        if (attributeName == null) {
            throw new NoSuchElementException("No match for FID=" + value);
        } else {
            throw new NoSuchElementException("No match for " + attributeName + "=" + value);
        }
    }

    public Feature feature(String typeName, String fid)
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureReader reader = reader(typeName);
        Feature f;

        try {
            while (reader.hasNext()) {
                f = reader.next();

                if (fid.equals(f.getID())) {
                    return f;
                }
            }
        } finally {
            reader.close();
        }

        return null;
    }

    public void testGetFeaturesWriterModify() throws IOException, IllegalAttributeException {
        FeatureWriter writer = writer("road");
        Feature feature;

        while (writer.hasNext()) {
            feature = writer.next();

            if (feature.getID().equals(roadFeatures[0].getID())) {
                feature.setAttribute("name", "changed");
                writer.write();
            }
        }

        feature = (Feature) feature("road", roadFeatures[0].getID());
        assertNotNull(feature);
        assertEquals("changed", feature.getAttribute("name"));
    }

    public void testGetFeatureWriterTypeNameTransaction()
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureWriter writer;

        writer = data.getFeatureWriter("road", Transaction.AUTO_COMMIT);
        assertEquals(roadFeatures.length, count(writer));

        //writer.close(); called by count.
    }

    public void testGetFeatureWriterAppendTypeNameTransaction() throws Exception {
        FeatureWriter writer;

        writer = data.getFeatureWriterAppend("road", Transaction.AUTO_COMMIT);
        assertEquals(0, count(writer));

        //writer.close(); called by count
    }

    /*
     * Test for FeatureWriter getFeatureWriter(String, boolean, Transaction)
     * @task REVISIT: JDBCDataStore currently does not return these proper
     * instanceof's.  If we want to guarantee that people can't append to
     * a request with a FeatureWriter then we could add the functionality to
     * JDBCDataStore by having getFeatureWriter(.. Filter ...) check to see if
     * the FeatureWriter returned is instanceof FilteringFeatureWriter, and if
     * not then just wrap it in a FilteringFeatureWriter(writer, Filter.INCLUDE).
     * I think it'd be a bit of unnecessary overhead, but if we want it it's
     * easy to do.  It will guarantee that calls with Filter won't ever append.
     * Doing with Filter.INCLUDE, however, would require a bit of reworking, as
     * the Filter getFeatureWriter is currently where we do the bulk of
     * the work.
     */
    public void testGetFeatureWriterFilter()
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureWriter writer;

        writer = data.getFeatureWriter("road", Filter.EXCLUDE, Transaction.AUTO_COMMIT);

        //see task above
        // assertTrue(writer instanceof EmptyFeatureWriter);
        assertEquals(0, count(writer));

        writer = data.getFeatureWriter("road", Filter.INCLUDE, Transaction.AUTO_COMMIT);

        //assertFalse(writer instanceof FilteringFeatureWriter);
        assertEquals(roadFeatures.length, count(writer));

        writer = data.getFeatureWriter("road", rd1Filter, Transaction.AUTO_COMMIT);

        //assertTrue(writer instanceof FilteringFeatureWriter);
        assertEquals(1, count(writer));
    }

// Uncomment when and if MySQLDataStore will support transaction
//    /**
//     * Test two transactions one removing feature, and one adding a feature.
//     *
//     * @throws Exception DOCUMENT ME!
//     */
//    public void testGetFeatureWriterTransaction() throws Exception {
//        Transaction t1 = new DefaultTransaction();
//        Transaction t2 = new DefaultTransaction();
//        FeatureWriter writer1 = data.getFeatureWriter("road", rd1Filter, t1);
//        FeatureWriter writer2 = data.getFeatureWriterAppend("road", t2);
//
//        FeatureType road = data.getSchema("road");
//        FeatureReader reader;
//        Feature feature;
//        Feature[] ORIGIONAL = roadFeatures;
//        Feature[] REMOVE = new Feature[ORIGIONAL.length - 1];
//        Feature[] ADD = new Feature[ORIGIONAL.length + 1];
//        Feature[] FINAL = new Feature[ORIGIONAL.length];
//        int i;
//        int index;
//        index = 0;
//
//        for (i = 0; i < ORIGIONAL.length; i++) {
//            feature = ORIGIONAL[i];
//
//            if (!feature.getID().equals(roadFeatures[0].getID())) {
//                REMOVE[index++] = feature;
//            }
//        }
//
//        for (i = 0; i < ORIGIONAL.length; i++) {
//            ADD[i] = ORIGIONAL[i];
//        }
//
//        ADD[i] = newRoad; // will need to update with Fid from database
//
//        for (i = 0; i < REMOVE.length; i++) {
//            FINAL[i] = REMOVE[i];
//        }
//
//        FINAL[i] = newRoad; // will need to update with Fid from database
//
//        // start of with ORIGINAL                        
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(covers(reader, ORIGIONAL));
//
//        // writer 1 removes road.rd1 on t1
//        // -------------------------------
//        // - tests transaction independence from DataStore
//        while (writer1.hasNext()) {
//            feature = writer1.next();
//            assertEquals(roadFeatures[0].getID(), feature.getID());
//            writer1.remove();
//        }
//
//        // still have ORIGIONAL and t1 has REMOVE
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(covers(reader, ORIGIONAL));
//
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t1);
//        assertTrue(covers(reader, REMOVE));
//
//        // close writer1
//        // --------------
//        // ensure that modification is left up to transaction commmit
//        writer1.close();
//
//        // We still have ORIGIONAL and t1 has REMOVE
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(covers(reader, ORIGIONAL));
//
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t1);
//        assertTrue(covers(reader, REMOVE));
//
//        // writer 2 adds road.rd4 on t2
//        // ----------------------------
//        // - tests transaction independence from each other
//        feature = writer2.next();
//        feature.setAttributes(newRoad.getAttributes(null));
//        writer2.write();
//
//        // HACK: ?!? update ADD and FINAL with new FID from database
//        //
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t2);
//        newRoad = findFeature(reader, "id", new Integer(4));
//        System.out.println("newRoad:" + newRoad);
//        ADD[ADD.length - 1] = newRoad;
//        FINAL[FINAL.length - 1] = newRoad;
//
//        // We still have ORIGIONAL and t2 has ADD
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(covers(reader, ORIGIONAL));
//
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t2);
//        assertMatched(ADD, reader); // broken due to FID problem
//
//        writer2.close();
//
//        // Still have ORIGIONAL and t2 has ADD
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(covers(reader, ORIGIONAL));
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t2);
//        assertTrue(coversLax(reader, ADD));
//
//        // commit t1
//        // ---------
//        // -ensure that delayed writing of transactions takes place
//        //
//        t1.commit();
//
//        // We now have REMOVE, as does t1 (which has not additional diffs)
//        // t2 will have FINAL
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(covers(reader, REMOVE));
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t1);
//        assertTrue(covers(reader, REMOVE));
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t2);
//        assertTrue(coversLax(reader, FINAL));
//
//        // commit t2
//        // ---------
//        // -ensure that everyone is FINAL at the end of the day
//        t2.commit();
//
//        // We now have Number( remove one and add one)
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        reader = data.getFeatureReader(road, Filter.INCLUDE, Transaction.AUTO_COMMIT);
//        assertTrue(coversLax(reader, FINAL));
//
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t1);
//        assertTrue(coversLax(reader, FINAL));
//
//        reader = data.getFeatureReader(road, Filter.INCLUDE, t2);
//        assertTrue(coversLax(reader, FINAL));
//    }

    // Feature Source Testing
    public void testGetFeatureSourceRoad() throws IOException {
        FeatureSource road = data.getFeatureSource("road");

        assertEquals(roadType, road.getSchema());
        assertSame(data, road.getDataStore());

        int count = road.getCount(Query.ALL);
        assertTrue((count == 3) || (count == -1));

        Envelope bounds = road.getBounds(Query.ALL);
        assertTrue((bounds == null) || bounds.equals(roadBounds));

        FeatureCollection all = road.getFeatures();
        assertEquals(3, all.size());
        assertEquals(roadBounds, all.getBounds());

        FeatureCollection expected = DataUtilities.collection(roadFeatures);

        assertCovers("all", expected, all);
        assertEquals(roadBounds, all.getBounds());

        FeatureCollection some = road.getFeatures(rd12Filter);
        assertEquals(2, some.size());

        Envelope e = new Envelope();
        e.expandToInclude(roadFeatures[0].getBounds());
        e.expandToInclude(roadFeatures[1].getBounds());
        assertEquals(e, some.getBounds());
        assertEquals(some.getSchema(), road.getSchema());

        DefaultQuery query = new DefaultQuery("road", rd12Filter, new String[] { "name" });

        FeatureCollection half = road.getFeatures(query);
        assertEquals(2, half.size());
        assertEquals(1, half.getSchema().getAttributeCount());

        FeatureIterator reader = half.features();
        FeatureType type = half.getSchema();
        reader.close();

        FeatureType actual = half.getSchema();

        assertEquals(type.getTypeName(), actual.getTypeName());
        assertEquals(type.getNamespace(), actual.getNamespace());
        assertEquals(type.getAttributeCount(), actual.getAttributeCount());

        for (int i = 0; i < type.getAttributeCount(); i++) {
            assertEquals(type.getAttributeType(i), actual.getAttributeType(i));
        }

        assertNull(type.getDefaultGeometry());
        assertEquals(type.getDefaultGeometry(), actual.getDefaultGeometry());
        assertEquals(type, actual);

        Envelope b = half.getBounds();
        assertEquals(roadBounds , b);
    }

    public void testGetFeatureSourceRiver()
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureSource river = data.getFeatureSource("river");

        assertEquals(riverType, river.getSchema());
        assertSame(data, river.getDataStore());

        FeatureCollection all = river.getFeatures();
        assertEquals(2, all.size());
        assertEquals(riverBounds, all.getBounds());
        assertTrue("rivers", covers(all.features(), riverFeatures));

        FeatureCollection expected = DataUtilities.collection(riverFeatures);
        assertCovers("all", expected, all);
        assertEquals(riverBounds, all.getBounds());
    }

    //
    // Feature Store Testing
    //
    public void testGetFeatureStoreModifyFeatures1() throws IOException {
        FeatureStore road = (FeatureStore) data.getFeatureSource("road");

        //FilterFactory factory = FilterFactoryFinder.createFilterFactory();
        //rd1Filter = factory.createFidFilter( roadFeatures[0].getID() );
        Object changed = new Integer(5);
        AttributeType name = roadType.getAttributeType("id");
        road.modifyFeatures(name, changed, rd1Filter);

        FeatureCollection results = road.getFeatures(rd1Filter);
        assertEquals(changed, results.features().next().getAttribute("id"));
    }

    public void testGetFeatureStoreModifyFeatures2() throws IOException {
        FeatureStore road = (FeatureStore) data.getFeatureSource("road");

        FilterFactory factory = FilterFactoryFinder.createFilterFactory();
        rd1Filter = factory.createFidFilter(roadFeatures[0].getID());

        AttributeType name = roadType.getAttributeType("name");
        road.modifyFeatures(new AttributeType[] { name, }, new Object[] { "changed", }, rd1Filter);

        FeatureCollection results = road.getFeatures(rd1Filter);
        assertEquals("changed", results.features().next().getAttribute("name"));
    }

    public void testGetFeatureStoreRemoveFeatures() throws IOException {
        FeatureStore road = (FeatureStore) data.getFeatureSource("road");

        road.removeFeatures(rd1Filter);
        assertEquals(0, road.getFeatures(rd1Filter).size());
        assertEquals(roadFeatures.length - 1, road.getFeatures().size());
    }

    public void testGetFeatureStoreAddFeatures() throws IOException {
        FeatureStore road = (FeatureStore) data.getFeatureSource("road");

        road.addFeatures(DataUtilities.collection( newRoad ));
        assertEquals(roadFeatures.length + 1, count("road"));
    }

    public void testGetFeatureStoreSetFeatures()
        throws NoSuchElementException, IOException, IllegalAttributeException {
        FeatureReader reader = DataUtilities.reader(new Feature[] { newRoad, });

        FeatureStore road = (FeatureStore) data.getFeatureSource("road");

        assertEquals(3, count("road"));

        road.setFeatures(reader);

        assertEquals(1, count("road"));
    }

//  Uncomment when and if MySQLDataStore will support transaction
//    public void testGetFeatureStoreTransactionSupport() throws Exception {
//        Transaction t1 = new DefaultTransaction();
//        Transaction t2 = new DefaultTransaction();
//
//        FeatureStore road = (FeatureStore) data.getFeatureSource("road");
//        FeatureStore road1 = (FeatureStore) data.getFeatureSource("road");
//        FeatureStore road2 = (FeatureStore) data.getFeatureSource("road");
//
//        road1.setTransaction(t1);
//        road2.setTransaction(t2);
//
//        Feature feature;
//        Feature[] ORIGIONAL = roadFeatures;
//        Feature[] REMOVE = new Feature[ORIGIONAL.length - 1];
//        Feature[] ADD = new Feature[ORIGIONAL.length + 1];
//        Feature[] FINAL = new Feature[ORIGIONAL.length];
//        int i;
//        int index;
//        index = 0;
//
//        for (i = 0; i < ORIGIONAL.length; i++) {
//            feature = ORIGIONAL[i];
//            LOGGER.info("id is " + feature.getID());
//
//            if (!feature.getID().equals("road.rd1")) {
//                REMOVE[index++] = feature;
//            }
//        }
//
//        for (i = 0; i < ORIGIONAL.length; i++) {
//            ADD[i] = ORIGIONAL[i];
//        }
//
//        ADD[i] = newRoad;
//
//        for (i = 0; i < REMOVE.length; i++) {
//            FINAL[i] = REMOVE[i];
//        }
//
//        FINAL[i] = newRoad;
//
//        // start of with ORIGINAL
//        assertTrue(covers(road.getFeatures().reader(), ORIGIONAL));
//
//        // road1 removes road.rd1 on t1
//        // -------------------------------
//        // - tests transaction independence from DataStore
//        road1.removeFeatures(rd1Filter);
//
//        // still have ORIGIONAL and t1 has REMOVE
//        assertTrue(covers(road.getFeatures().reader(), ORIGIONAL));
//        assertTrue(covers(road1.getFeatures().reader(), REMOVE));
//
//        // road2 adds road.rd4 on t2
//        // ----------------------------
//        // - tests transaction independence from each other
//        FeatureReader reader = DataUtilities.reader(new Feature[] { newRoad, });
//        road2.addFeatures(reader);
//
//        // We still have ORIGIONAL, t1 has REMOVE, and t2 has ADD
//        assertTrue(covers(road.getFeatures().reader(), ORIGIONAL));
//        assertTrue(covers(road1.getFeatures().reader(), REMOVE));
//        assertTrue(coversLax(road2.getFeatures().reader(), ADD));
//
//        // commit t1
//        // ---------
//        // -ensure that delayed writing of transactions takes place
//        //
//        t1.commit();
//
//        // We now have REMOVE, as does t1 (which has not additional diffs)
//        // t2 will have FINAL
//        assertTrue(covers(road.getFeatures().reader(), REMOVE));
//        assertTrue(covers(road1.getFeatures().reader(), REMOVE));
//        assertTrue(coversLax(road2.getFeatures().reader(), FINAL));
//
//        // commit t2
//        // ---------
//        // -ensure that everyone is FINAL at the end of the day
//        t2.commit();
//
//        // We now have Number( remove one and add one)
//        assertTrue(coversLax(road.getFeatures().reader(), FINAL));
//        assertTrue(coversLax(road1.getFeatures().reader(), FINAL));
//        assertTrue(coversLax(road2.getFeatures().reader(), FINAL));
//    }

    boolean isLocked(String typeName, String fid) {
        InProcessLockingManager lockingManager = (InProcessLockingManager) data.getLockingManager();

        return lockingManager.isLocked(typeName, fid);
    }

    //
    // FeatureLocking Testing    
    //

    /*
     * Test for void lockFeatures()
     */
    public void testLockFeatures() throws IOException {
        FeatureLock lock = FeatureLockFactory.generate("test", LOCK_DURATION);
        FeatureLocking road = (FeatureLocking) data.getFeatureSource("road");
        road.setFeatureLock(lock);

        assertFalse(isLocked("road", "road.rd1"));
        road.lockFeatures();
        assertTrue(isLocked("road", "road.rd1"));
    }

    public void testUnLockFeatures() throws IOException {
        FeatureLock lock = FeatureLockFactory.generate("test", LOCK_DURATION);
        FeatureLocking road = (FeatureLocking) data.getFeatureSource("road");
        road.setFeatureLock(lock);
        road.lockFeatures();

        try {
            road.unLockFeatures();
            fail("unlock should fail due on AUTO_COMMIT");
        } catch (IOException expected) {
        }

        Transaction t = new DefaultTransaction();
        road.setTransaction(t);

        try {
            road.unLockFeatures();
            fail("unlock should fail due lack of authorization");
        } catch (IOException expected) {
        }

        t.addAuthorization(lock.getAuthorization());
        road.unLockFeatures();
    }

    public void testLockFeatureInteraction() throws IOException {
        FeatureLock lockA = FeatureLockFactory.generate("LockA", LOCK_DURATION);
        FeatureLock lockB = FeatureLockFactory.generate("LockB", LOCK_DURATION);
        Transaction t1 = new DefaultTransaction();
        Transaction t2 = new DefaultTransaction();
        FeatureLocking road1 = (FeatureLocking) data.getFeatureSource("road");
        FeatureLocking road2 = (FeatureLocking) data.getFeatureSource("road");
        road1.setTransaction(t1);
        road2.setTransaction(t2);
        road1.setFeatureLock(lockA);
        road2.setFeatureLock(lockB);

        assertFalse(isLocked("road", "road.rd1"));
        assertFalse(isLocked("road", "road.rd2"));
        assertFalse(isLocked("road", "road.rd3"));

        road1.lockFeatures(rd1Filter);
        assertTrue(isLocked("road", "road.rd1"));
        assertFalse(isLocked("road", "road.rd2"));
        assertFalse(isLocked("road", "road.rd3"));

        road2.lockFeatures(rd2Filter);
        assertTrue(isLocked("road", "road.rd1"));
        assertTrue(isLocked("road", "road.rd2"));
        assertFalse(isLocked("road", "road.rd3"));

        try {
            road1.unLockFeatures(rd1Filter);
            fail("need authorization");
        } catch (IOException expected) {
        }

        t1.addAuthorization(lockA.getAuthorization());

        try {
            road1.unLockFeatures(rd2Filter);
            fail("need correct authorization");
        } catch (IOException expected) {
        }

        road1.unLockFeatures(rd1Filter);
        assertFalse(isLocked("road", "road.rd1"));
        assertTrue(isLocked("road", "road.rd2"));
        assertFalse(isLocked("road", "road.rd3"));

        t2.addAuthorization(lockB.getAuthorization());
        road2.unLockFeatures(rd2Filter);
        assertFalse(isLocked("road", "road.rd1"));
        assertFalse(isLocked("road", "road.rd2"));
        assertFalse(isLocked("road", "road.rd3"));
    }

    public void testGetFeatureLockingExpire() throws Exception {
        FeatureLock lock = FeatureLockFactory.generate("Timed", 200);

        FeatureLocking road = (FeatureLocking) data.getFeatureSource("road");
        road.setFeatureLock(lock);
        assertFalse(isLocked("road", "road.rd1"));

        road.lockFeatures(rd1Filter);
        assertTrue(isLocked("road", "road.rd1"));
        long then = System.currentTimeMillis();
        do {
            Thread.sleep( 200 );
        } while ( System.currentTimeMillis() - then < 200 ); 
        assertFalse(isLocked("road", "road.rd1"));
    }
}
