You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2015/04/30 17:59:11 UTC

[1/2] cayenne git commit: CAY-2008 Connection pool refactoring and validation query support in Cayenne DataSource

Repository: cayenne
Updated Branches:
  refs/heads/master 5e037fc73 -> 8b54c052d


http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
index dbf985e..88bf277 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextIT.java
@@ -19,6 +19,29 @@
 
 package org.apache.cayenne.access;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 import org.apache.cayenne.Cayenne;
 import org.apache.cayenne.DataObject;
 import org.apache.cayenne.DataRow;
@@ -28,7 +51,6 @@ import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.ResultBatchIterator;
 import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.ResultIteratorCallback;
-import org.apache.cayenne.conn.PoolManager;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
@@ -59,575 +81,552 @@ import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.math.BigDecimal;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class DataContextIT extends ServerCase {
 
-    @Inject
-    protected DataContext context;
-
-    @Inject
-    protected DBHelper dbHelper;
-
-    @Inject
-    protected UnitDbAdapter accessStackAdapter;
-
-    @Inject
-    protected DataChannelInterceptor queryInterceptor;
-
-    @Inject
-    protected ServerCaseDataSourceFactory dataSourceFactory;
-
-    protected TableHelper tArtist;
-    protected TableHelper tExhibit;
-    protected TableHelper tGallery;
-    protected TableHelper tPainting;
-
-    @Before
-    public void setUp() throws Exception {
-        tArtist = new TableHelper(dbHelper, "ARTIST");
-        tArtist.setColumns("ARTIST_ID", "ARTIST_NAME");
-
-        tExhibit = new TableHelper(dbHelper, "EXHIBIT");
-        tExhibit.setColumns("EXHIBIT_ID", "GALLERY_ID", "OPENING_DATE", "CLOSING_DATE");
-
-        tGallery = new TableHelper(dbHelper, "GALLERY");
-        tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
-
-        tPainting = new TableHelper(dbHelper, "PAINTING");
-        tPainting.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "ESTIMATED_PRICE");
-    }
-
-    protected void createSingleArtistDataSet() throws Exception {
-        tArtist.insert(33001, "artist1");
-    }
-
-    protected void createFiveArtistDataSet_MixedCaseName() throws Exception {
-        tArtist.insert(33001, "artist1");
-        tArtist.insert(33002, "Artist3");
-        tArtist.insert(33003, "aRtist5");
-        tArtist.insert(33004, "arTist2");
-        tArtist.insert(33005, "artISt4");
-    }
-
-    protected void createGalleriesAndExhibitsDataSet() throws Exception {
-
-        tGallery.insert(33001, "gallery1");
-        tGallery.insert(33002, "gallery2");
-        tGallery.insert(33003, "gallery3");
-        tGallery.insert(33004, "gallery4");
-
-        Timestamp now = new Timestamp(System.currentTimeMillis());
-
-        tExhibit.insert(1, 33001, now, now);
-        tExhibit.insert(2, 33002, now, now);
-    }
-
-    protected void createArtistsDataSet() throws Exception {
-        tArtist.insert(33001, "artist1");
-        tArtist.insert(33002, "artist2");
-        tArtist.insert(33003, "artist3");
-        tArtist.insert(33004, "artist4");
-        tArtist.insert(33005, "artist5");
-        tArtist.insert(33006, "artist11");
-        tArtist.insert(33007, "artist21");
-    }
-
-    protected void createLargeArtistsDataSet() throws Exception {
-        for (int i = 1; i <= 20; i++) {
-            tArtist.insert(i, "artist" + i);
-        }
-    }
-
-    protected void createArtistsAndPaintingsDataSet() throws Exception {
-        createArtistsDataSet();
-
-        tPainting.insert(33001, "P_artist1", 33001, 1000);
-        tPainting.insert(33002, "P_artist2", 33002, 2000);
-        tPainting.insert(33003, "P_artist3", 33003, 3000);
-        tPainting.insert(33004, "P_artist4", 33004, 4000);
-        tPainting.insert(33005, "P_artist5", 33005, 5000);
-        tPainting.insert(33006, "P_artist11", 33006, 11000);
-        tPainting.insert(33007, "P_artist21", 33007, 21000);
-    }
-
-    @Test
-    public void testCurrentSnapshot1() throws Exception {
-        createSingleArtistDataSet();
-
-        SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
-                "artist1"));
-        Artist artist = (Artist) context.performQuery(query).get(0);
-
-        DataRow snapshot = context.currentSnapshot(artist);
-        assertEquals(artist.getArtistName(), snapshot.get("ARTIST_NAME"));
-        assertEquals(artist.getDateOfBirth(), snapshot.get("DATE_OF_BIRTH"));
-    }
-
-    @Test
-    public void testCurrentSnapshot2() throws Exception {
-        createSingleArtistDataSet();
-
-        // test null values
-        SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
-                "artist1"));
-        Artist artist = (Artist) context.performQuery(query).get(0);
-
-        artist.setArtistName(null);
-        artist.setDateOfBirth(null);
-
-        DataRow snapshot = context.currentSnapshot(artist);
-        assertTrue(snapshot.containsKey("ARTIST_NAME"));
-        assertNull(snapshot.get("ARTIST_NAME"));
-
-        assertTrue(snapshot.containsKey("DATE_OF_BIRTH"));
-        assertNull(snapshot.get("DATE_OF_BIRTH"));
-    }
-
-    @Test
-    public void testCurrentSnapshot3() throws Exception {
-        createSingleArtistDataSet();
-
-        // test null values
-        SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
-                "artist1"));
-        Artist artist = (Artist) context.performQuery(query).get(0);
-
-        // test FK relationship snapshotting
-        Painting p1 = new Painting();
-        context.registerNewObject(p1);
-        p1.setToArtist(artist);
-
-        DataRow s1 = context.currentSnapshot(p1);
-        Map<String, Object> idMap = artist.getObjectId().getIdSnapshot();
-        assertEquals(idMap.get("ARTIST_ID"), s1.get("ARTIST_ID"));
-    }
-
-    /**
-     * Testing snapshot with to-one fault. This was a bug CAY-96.
-     */
-    @Test
-    public void testCurrentSnapshotWithToOneFault() throws Exception {
-
-        createGalleriesAndExhibitsDataSet();
-
-        // Exhibit with Gallery as Fault must still include Gallery
-        // Artist and Exhibit (Exhibit has unresolved to-one to gallery as in
-        // the
-        // CAY-96 bug report)
-
-        ObjectId eId = new ObjectId("Exhibit", Exhibit.EXHIBIT_ID_PK_COLUMN, 2);
-        Exhibit e = (Exhibit) context.performQuery(new ObjectIdQuery(eId)).get(0);
-
-        assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY_PROPERTY) instanceof Fault);
-
-        DataRow snapshot = context.currentSnapshot(e);
-
-        // assert that after taking a snapshot, we have FK in, but the
-        // relationship
-        // is still a Fault
-        assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY_PROPERTY) instanceof Fault);
-        assertEquals(new Integer(33002), snapshot.get("GALLERY_ID"));
-    }
-
-    /**
-     * Tests how CHAR field is handled during fetch. Some databases (Oracle...)
-     * would pad a CHAR column with extra spaces, returned to the client.
-     * Cayenne should trim it.
-     */
-    @Test
-    public void testCharFetch() throws Exception {
-        createSingleArtistDataSet();
-
-        SelectQuery query = new SelectQuery(Artist.class);
-        Artist a = (Artist) context.performQuery(query).get(0);
-        assertEquals(a.getArtistName().trim(), a.getArtistName());
-    }
-
-    /**
-     * Tests how CHAR field is handled during fetch in the WHERE clause. Some
-     * databases (Oracle...) would pad a CHAR column with extra spaces, returned
-     * to the client. Cayenne should trim it.
-     */
-    @Test
-    public void testCharInQualifier() throws Exception {
-        createArtistsDataSet();
-
-        Expression e = ExpressionFactory.matchExp("artistName", "artist1");
-        SelectQuery q = new SelectQuery(Artist.class, e);
-        List<Artist> artists = context.performQuery(q);
-        assertEquals(1, artists.size());
-    }
-
-    /**
-     * Test fetching query with multiple relationship paths between the same 2
-     * entities used in qualifier.
-     */
-    @Test
-    public void testMultiObjRelFetch() throws Exception {
-        createArtistsAndPaintingsDataSet();
-
-        SelectQuery q = new SelectQuery(Painting.class);
-        q.andQualifier(ExpressionFactory.matchExp("toArtist.artistName", "artist2"));
-        q.orQualifier(ExpressionFactory.matchExp("toArtist.artistName", "artist4"));
-        List<Painting> results = context.performQuery(q);
-
-        assertEquals(2, results.size());
-    }
-
-    /**
-     * Test fetching query with multiple relationship paths between the same 2
-     * entities used in qualifier.
-     */
-    @Test
-    public void testMultiDbRelFetch() throws Exception {
-        createArtistsAndPaintingsDataSet();
-
-        SelectQuery q = new SelectQuery("Painting");
-        q.andQualifier(ExpressionFactory.matchDbExp("toArtist.ARTIST_NAME", "artist2"));
-        q.orQualifier(ExpressionFactory.matchDbExp("toArtist.ARTIST_NAME", "artist4"));
-        List<?> results = context.performQuery(q);
-
-        assertEquals(2, results.size());
-    }
-
-    @Test
-    public void testSelectDate() throws Exception {
-        createGalleriesAndExhibitsDataSet();
-
-        List<Exhibit> objects = context.performQuery(new SelectQuery(Exhibit.class));
-        assertFalse(objects.isEmpty());
-
-        Exhibit e1 = objects.get(0);
-        assertEquals(java.util.Date.class, e1.getClosingDate().getClass());
-    }
-
-    @Test
-    public void testCaseInsensitiveOrdering() throws Exception {
-        if (!accessStackAdapter.supportsCaseInsensitiveOrder()) {
-            return;
-        }
-
-        createFiveArtistDataSet_MixedCaseName();
-
-        // case insensitive ordering appends extra columns
-        // to the query when query is using DISTINCT...
-        // verify that the result is not messed up
-
-        SelectQuery query = new SelectQuery(Artist.class);
-        Ordering ordering = new Ordering(Artist.ARTIST_NAME_PROPERTY, SortOrder.ASCENDING_INSENSITIVE);
-        query.addOrdering(ordering);
-        query.setDistinct(true);
-
-        List<Artist> objects = context.performQuery(query);
-        assertEquals(5, objects.size());
-
-        Artist artist = objects.get(0);
-        DataRow snapshot = context.getObjectStore().getSnapshot(artist.getObjectId());
-        assertEquals(3, snapshot.size());
-
-        // assert the ordering
-        assertEquals("artist1", objects.get(0).getArtistName());
-        assertEquals("arTist2", objects.get(1).getArtistName());
-        assertEquals("Artist3", objects.get(2).getArtistName());
-        assertEquals("artISt4", objects.get(3).getArtistName());
-        assertEquals("aRtist5", objects.get(4).getArtistName());
-    }
-
-    @Test
-    public void testSelect_DataRows() throws Exception {
-        createArtistsAndPaintingsDataSet();
-
-        SelectQuery<DataRow> query = SelectQuery.dataRowQuery(Artist.class, null);
-        List<DataRow> objects = context.select(query);
-
-        assertNotNull(objects);
-        assertEquals(7, objects.size());
-        assertTrue("DataRow expected, got " + objects.get(0).getClass(), objects.get(0) instanceof DataRow);
-    }
-
-    @Test
-    public void testPerformSelectQuery1() throws Exception {
-        createArtistsAndPaintingsDataSet();
-
-        SelectQuery query = new SelectQuery(Artist.class);
-        List<?> objects = context.performQuery(query);
-
-        assertNotNull(objects);
-        assertEquals(7, objects.size());
-        assertTrue("Artist expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Artist);
-    }
-
-    @Test
-    public void testPerformSelectQuery2() throws Exception {
-        createArtistsAndPaintingsDataSet();
-
-        // do a query with complex qualifier
-        List<Expression> expressions = new ArrayList<Expression>();
-        expressions.add(ExpressionFactory.matchExp("artistName", "artist3"));
-        expressions.add(ExpressionFactory.matchExp("artistName", "artist5"));
-        expressions.add(ExpressionFactory.matchExp("artistName", "artist21"));
-
-        SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.joinExp(Expression.OR, expressions));
-
-        List<?> objects = context.performQuery(query);
-
-        assertNotNull(objects);
-        assertEquals(3, objects.size());
-        assertTrue("Artist expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Artist);
-    }
-
-    @Test
-    public void testPerformQuery_Routing() {
-        Query query = mock(Query.class);
-        QueryMetadata md = mock(QueryMetadata.class);
-        when(query.getMetaData(any(EntityResolver.class))).thenReturn(md);
-        context.performGenericQuery(query);
-        verify(query).route(any(QueryRouter.class), eq(context.getEntityResolver()), (Query) isNull());
-    }
-
-    @Test
-    public void testPerformNonSelectingQuery() throws Exception {
-
-        createSingleArtistDataSet();
-
-        SelectQuery select = new SelectQuery(Painting.class, Expression.fromString("db:PAINTING_ID = 1"));
-
-        assertEquals(0, context.performQuery(select).size());
-
-        SQLTemplate query = new SQLTemplate(Painting.class,
-                "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
-                        + "VALUES (1, 'PX', 33001, 1)");
-        context.performNonSelectingQuery(query);
-        assertEquals(1, context.performQuery(select).size());
-    }
-
-    @Test
-    public void testPerformNonSelectingQueryCounts1() throws Exception {
-        createArtistsDataSet();
-
-        SQLTemplate query = new SQLTemplate(Painting.class,
-                "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
-                        + "VALUES ($pid, '$pt', $aid, $price)");
-
-        Map<String, Object> map = new HashMap<String, Object>();
-        map.put("pid", new Integer(1));
-        map.put("pt", "P1");
-        map.put("aid", new Integer(33002));
-        map.put("price", new Double(1.1));
-
-        // single batch of parameters
-        query.setParameters(map);
-
-        int[] counts = context.performNonSelectingQuery(query);
-        assertNotNull(counts);
-        assertEquals(1, counts.length);
-        assertEquals(1, counts[0]);
-    }
-
-    @Test
-    public void testPerformNonSelectingQueryCounts2() throws Exception {
-
-        createArtistsDataSet();
-
-        SQLTemplate query = new SQLTemplate(Painting.class,
-                "INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
-                        + "VALUES ($pid, '$pt', $aid, #bind($price 'DECIMAL' 2))");
-
-        Map<String, Object>[] maps = new Map[3];
-        for (int i = 0; i < maps.length; i++) {
-            maps[i] = new HashMap<String, Object>();
-            maps[i].put("pid", new Integer(1 + i));
-            maps[i].put("pt", "P-" + i);
-            maps[i].put("aid", new Integer(33002));
-            maps[i].put("price", new BigDecimal("1." + i));
-        }
-
-        // single batch of parameters
-        query.setParameters(maps);
-
-        int[] counts = context.performNonSelectingQuery(query);
-        assertNotNull(counts);
-        assertEquals(maps.length, counts.length);
-        for (int i = 0; i < maps.length; i++) {
-            assertEquals(1, counts[i]);
-        }
-
-        SQLTemplate delete = new SQLTemplate(Painting.class, "delete from PAINTING");
-        counts = context.performNonSelectingQuery(delete);
-        assertNotNull(counts);
-        assertEquals(1, counts.length);
-        assertEquals(3, counts[0]);
-    }
-
-    @Test
-    public void testPerformPaginatedQuery() throws Exception {
-        createArtistsDataSet();
-
-        SelectQuery<Artist> query = SelectQuery.query(Artist.class);
-        query.setPageSize(5);
-        List<Artist> objects = context.select(query);
-        assertNotNull(objects);
-        assertTrue(objects instanceof IncrementalFaultList<?>);
-        assertTrue(((IncrementalFaultList<Artist>) objects).elements.get(0) instanceof Long);
-        assertTrue(((IncrementalFaultList<Artist>) objects).elements.get(6) instanceof Long);
-
-        assertTrue(objects.get(0) instanceof Artist);
-    }
-
-    @Test
-    public void testPerformPaginatedQuery1() throws Exception {
-        createArtistsDataSet();
-
-        EJBQLQuery query = new EJBQLQuery("select a FROM Artist a");
-        query.setPageSize(5);
-        List<?> objects = context.performQuery(query);
-        assertNotNull(objects);
-        assertTrue(objects instanceof IncrementalFaultList<?>);
-        assertTrue(((IncrementalFaultList<?>) objects).elements.get(0) instanceof Long);
-        assertTrue(((IncrementalFaultList<?>) objects).elements.get(6) instanceof Long);
-
-        assertTrue(objects.get(0) instanceof Artist);
-    }
-
-    @Test
-    public void testPerformPaginatedQueryBigPage() throws Exception {
-        createArtistsDataSet();
-
-        SelectQuery query = new SelectQuery(Artist.class);
-        query.setPageSize(5);
-        final List<?> objects = context.performQuery(query);
-        assertNotNull(objects);
-        assertTrue(objects instanceof IncrementalFaultList<?>);
-
-        queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
-
-            public void execute() {
-                assertEquals(7, objects.size());
-            }
-        });
-    }
-
-    @Test
-    public void testPerformDataRowQuery() throws Exception {
-
-        createArtistsDataSet();
-
-        SelectQuery query = new SelectQuery(Artist.class);
-        query.setFetchingDataRows(true);
-        List<?> objects = context.performQuery(query);
-
-        assertNotNull(objects);
-        assertEquals(7, objects.size());
-        assertTrue("Map expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Map<?, ?>);
-    }
-
-    @Test
-    public void testCommitChangesRO1() throws Exception {
-
-        ROArtist a1 = (ROArtist) context.newObject("ROArtist");
-        a1.writePropertyDirectly("artistName", "abc");
-        a1.setPersistenceState(PersistenceState.MODIFIED);
-
-        try {
-            context.commitChanges();
-            fail("Inserting a 'read-only' object must fail.");
-        } catch (Exception ex) {
-            // exception is expected,
-            // must blow on saving new "read-only" object.
-        }
-    }
-
-    @Test
-    public void testCommitChangesRO2() throws Exception {
-        createArtistsDataSet();
-
-        SelectQuery query = new SelectQuery(ROArtist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
-                "artist1"));
-        ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
-        a1.writeProperty(ROArtist.ARTIST_NAME_PROPERTY, "abc");
-
-        try {
-            context.commitChanges();
-            fail("Updating a 'read-only' object must fail.");
-        } catch (Exception ex) {
-            // exception is expected,
-            // must blow on saving new "read-only" object.
-        }
-    }
-
-    @Test
-    public void testCommitChangesRO3() throws Exception {
-
-        createArtistsDataSet();
-
-        SelectQuery query = new SelectQuery(ROArtist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
-                "artist1"));
-        ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
-        context.deleteObjects(a1);
-
-        try {
-            context.commitChanges();
-            fail("Deleting a 'read-only' object must fail.");
-        } catch (Exception ex) {
-            // exception is expected,
-            // must blow on saving new "read-only" object.
-        }
-    }
-
-    @Test
-    public void testCommitChangesRO4() throws Exception {
-        createArtistsDataSet();
-
-        SelectQuery query = new SelectQuery(ROArtist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
-                "artist1"));
-        ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
-
-        Painting painting = context.newObject(Painting.class);
-        painting.setPaintingTitle("paint");
-        a1.addToPaintingArray(painting);
-
-        assertEquals(PersistenceState.MODIFIED, a1.getPersistenceState());
-        try {
-            context.commitChanges();
-        } catch (Exception ex) {
-            fail("Updating 'read-only' object's to-many must succeed, instead an exception was thrown: " + ex);
-        }
-
-        assertEquals(PersistenceState.COMMITTED, a1.getPersistenceState());
-    }
-
-    @Test
+	@Inject
+	protected DataContext context;
+
+	@Inject
+	protected DBHelper dbHelper;
+
+	@Inject
+	protected UnitDbAdapter accessStackAdapter;
+
+	@Inject
+	protected DataChannelInterceptor queryInterceptor;
+
+	@Inject
+	protected ServerCaseDataSourceFactory dataSourceFactory;
+
+	protected TableHelper tArtist;
+	protected TableHelper tExhibit;
+	protected TableHelper tGallery;
+	protected TableHelper tPainting;
+
+	@Before
+	public void setUp() throws Exception {
+		tArtist = new TableHelper(dbHelper, "ARTIST");
+		tArtist.setColumns("ARTIST_ID", "ARTIST_NAME");
+
+		tExhibit = new TableHelper(dbHelper, "EXHIBIT");
+		tExhibit.setColumns("EXHIBIT_ID", "GALLERY_ID", "OPENING_DATE", "CLOSING_DATE");
+
+		tGallery = new TableHelper(dbHelper, "GALLERY");
+		tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
+
+		tPainting = new TableHelper(dbHelper, "PAINTING");
+		tPainting.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "ESTIMATED_PRICE");
+	}
+
+	protected void createSingleArtistDataSet() throws Exception {
+		tArtist.insert(33001, "artist1");
+	}
+
+	protected void createFiveArtistDataSet_MixedCaseName() throws Exception {
+		tArtist.insert(33001, "artist1");
+		tArtist.insert(33002, "Artist3");
+		tArtist.insert(33003, "aRtist5");
+		tArtist.insert(33004, "arTist2");
+		tArtist.insert(33005, "artISt4");
+	}
+
+	protected void createGalleriesAndExhibitsDataSet() throws Exception {
+
+		tGallery.insert(33001, "gallery1");
+		tGallery.insert(33002, "gallery2");
+		tGallery.insert(33003, "gallery3");
+		tGallery.insert(33004, "gallery4");
+
+		Timestamp now = new Timestamp(System.currentTimeMillis());
+
+		tExhibit.insert(1, 33001, now, now);
+		tExhibit.insert(2, 33002, now, now);
+	}
+
+	protected void createArtistsDataSet() throws Exception {
+		tArtist.insert(33001, "artist1");
+		tArtist.insert(33002, "artist2");
+		tArtist.insert(33003, "artist3");
+		tArtist.insert(33004, "artist4");
+		tArtist.insert(33005, "artist5");
+		tArtist.insert(33006, "artist11");
+		tArtist.insert(33007, "artist21");
+	}
+
+	protected void createLargeArtistsDataSet() throws Exception {
+		for (int i = 1; i <= 20; i++) {
+			tArtist.insert(i, "artist" + i);
+		}
+	}
+
+	protected void createArtistsAndPaintingsDataSet() throws Exception {
+		createArtistsDataSet();
+
+		tPainting.insert(33001, "P_artist1", 33001, 1000);
+		tPainting.insert(33002, "P_artist2", 33002, 2000);
+		tPainting.insert(33003, "P_artist3", 33003, 3000);
+		tPainting.insert(33004, "P_artist4", 33004, 4000);
+		tPainting.insert(33005, "P_artist5", 33005, 5000);
+		tPainting.insert(33006, "P_artist11", 33006, 11000);
+		tPainting.insert(33007, "P_artist21", 33007, 21000);
+	}
+
+	@Test
+	public void testCurrentSnapshot1() throws Exception {
+		createSingleArtistDataSet();
+
+		SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
+				"artist1"));
+		Artist artist = (Artist) context.performQuery(query).get(0);
+
+		DataRow snapshot = context.currentSnapshot(artist);
+		assertEquals(artist.getArtistName(), snapshot.get("ARTIST_NAME"));
+		assertEquals(artist.getDateOfBirth(), snapshot.get("DATE_OF_BIRTH"));
+	}
+
+	@Test
+	public void testCurrentSnapshot2() throws Exception {
+		createSingleArtistDataSet();
+
+		// test null values
+		SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
+				"artist1"));
+		Artist artist = (Artist) context.performQuery(query).get(0);
+
+		artist.setArtistName(null);
+		artist.setDateOfBirth(null);
+
+		DataRow snapshot = context.currentSnapshot(artist);
+		assertTrue(snapshot.containsKey("ARTIST_NAME"));
+		assertNull(snapshot.get("ARTIST_NAME"));
+
+		assertTrue(snapshot.containsKey("DATE_OF_BIRTH"));
+		assertNull(snapshot.get("DATE_OF_BIRTH"));
+	}
+
+	@Test
+	public void testCurrentSnapshot3() throws Exception {
+		createSingleArtistDataSet();
+
+		// test null values
+		SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
+				"artist1"));
+		Artist artist = (Artist) context.performQuery(query).get(0);
+
+		// test FK relationship snapshotting
+		Painting p1 = new Painting();
+		context.registerNewObject(p1);
+		p1.setToArtist(artist);
+
+		DataRow s1 = context.currentSnapshot(p1);
+		Map<String, Object> idMap = artist.getObjectId().getIdSnapshot();
+		assertEquals(idMap.get("ARTIST_ID"), s1.get("ARTIST_ID"));
+	}
+
+	/**
+	 * Testing snapshot with to-one fault. This was a bug CAY-96.
+	 */
+	@Test
+	public void testCurrentSnapshotWithToOneFault() throws Exception {
+
+		createGalleriesAndExhibitsDataSet();
+
+		// Exhibit with Gallery as Fault must still include Gallery
+		// Artist and Exhibit (Exhibit has unresolved to-one to gallery as in
+		// the
+		// CAY-96 bug report)
+
+		ObjectId eId = new ObjectId("Exhibit", Exhibit.EXHIBIT_ID_PK_COLUMN, 2);
+		Exhibit e = (Exhibit) context.performQuery(new ObjectIdQuery(eId)).get(0);
+
+		assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY_PROPERTY) instanceof Fault);
+
+		DataRow snapshot = context.currentSnapshot(e);
+
+		// assert that after taking a snapshot, we have FK in, but the
+		// relationship
+		// is still a Fault
+		assertTrue(e.readPropertyDirectly(Exhibit.TO_GALLERY_PROPERTY) instanceof Fault);
+		assertEquals(new Integer(33002), snapshot.get("GALLERY_ID"));
+	}
+
+	/**
+	 * Tests how CHAR field is handled during fetch. Some databases (Oracle...)
+	 * would pad a CHAR column with extra spaces, returned to the client.
+	 * Cayenne should trim it.
+	 */
+	@Test
+	public void testCharFetch() throws Exception {
+		createSingleArtistDataSet();
+
+		SelectQuery query = new SelectQuery(Artist.class);
+		Artist a = (Artist) context.performQuery(query).get(0);
+		assertEquals(a.getArtistName().trim(), a.getArtistName());
+	}
+
+	/**
+	 * Tests how CHAR field is handled during fetch in the WHERE clause. Some
+	 * databases (Oracle...) would pad a CHAR column with extra spaces, returned
+	 * to the client. Cayenne should trim it.
+	 */
+	@Test
+	public void testCharInQualifier() throws Exception {
+		createArtistsDataSet();
+
+		Expression e = ExpressionFactory.matchExp("artistName", "artist1");
+		SelectQuery q = new SelectQuery(Artist.class, e);
+		List<Artist> artists = context.performQuery(q);
+		assertEquals(1, artists.size());
+	}
+
+	/**
+	 * Test fetching query with multiple relationship paths between the same 2
+	 * entities used in qualifier.
+	 */
+	@Test
+	public void testMultiObjRelFetch() throws Exception {
+		createArtistsAndPaintingsDataSet();
+
+		SelectQuery q = new SelectQuery(Painting.class);
+		q.andQualifier(ExpressionFactory.matchExp("toArtist.artistName", "artist2"));
+		q.orQualifier(ExpressionFactory.matchExp("toArtist.artistName", "artist4"));
+		List<Painting> results = context.performQuery(q);
+
+		assertEquals(2, results.size());
+	}
+
+	/**
+	 * Test fetching query with multiple relationship paths between the same 2
+	 * entities used in qualifier.
+	 */
+	@Test
+	public void testMultiDbRelFetch() throws Exception {
+		createArtistsAndPaintingsDataSet();
+
+		SelectQuery q = new SelectQuery("Painting");
+		q.andQualifier(ExpressionFactory.matchDbExp("toArtist.ARTIST_NAME", "artist2"));
+		q.orQualifier(ExpressionFactory.matchDbExp("toArtist.ARTIST_NAME", "artist4"));
+		List<?> results = context.performQuery(q);
+
+		assertEquals(2, results.size());
+	}
+
+	@Test
+	public void testSelectDate() throws Exception {
+		createGalleriesAndExhibitsDataSet();
+
+		List<Exhibit> objects = context.performQuery(new SelectQuery(Exhibit.class));
+		assertFalse(objects.isEmpty());
+
+		Exhibit e1 = objects.get(0);
+		assertEquals(java.util.Date.class, e1.getClosingDate().getClass());
+	}
+
+	@Test
+	public void testCaseInsensitiveOrdering() throws Exception {
+		if (!accessStackAdapter.supportsCaseInsensitiveOrder()) {
+			return;
+		}
+
+		createFiveArtistDataSet_MixedCaseName();
+
+		// case insensitive ordering appends extra columns
+		// to the query when query is using DISTINCT...
+		// verify that the result is not messed up
+
+		SelectQuery query = new SelectQuery(Artist.class);
+		Ordering ordering = new Ordering(Artist.ARTIST_NAME_PROPERTY, SortOrder.ASCENDING_INSENSITIVE);
+		query.addOrdering(ordering);
+		query.setDistinct(true);
+
+		List<Artist> objects = context.performQuery(query);
+		assertEquals(5, objects.size());
+
+		Artist artist = objects.get(0);
+		DataRow snapshot = context.getObjectStore().getSnapshot(artist.getObjectId());
+		assertEquals(3, snapshot.size());
+
+		// assert the ordering
+		assertEquals("artist1", objects.get(0).getArtistName());
+		assertEquals("arTist2", objects.get(1).getArtistName());
+		assertEquals("Artist3", objects.get(2).getArtistName());
+		assertEquals("artISt4", objects.get(3).getArtistName());
+		assertEquals("aRtist5", objects.get(4).getArtistName());
+	}
+
+	@Test
+	public void testSelect_DataRows() throws Exception {
+		createArtistsAndPaintingsDataSet();
+
+		SelectQuery<DataRow> query = SelectQuery.dataRowQuery(Artist.class, null);
+		List<DataRow> objects = context.select(query);
+
+		assertNotNull(objects);
+		assertEquals(7, objects.size());
+		assertTrue("DataRow expected, got " + objects.get(0).getClass(), objects.get(0) instanceof DataRow);
+	}
+
+	@Test
+	public void testPerformSelectQuery1() throws Exception {
+		createArtistsAndPaintingsDataSet();
+
+		SelectQuery query = new SelectQuery(Artist.class);
+		List<?> objects = context.performQuery(query);
+
+		assertNotNull(objects);
+		assertEquals(7, objects.size());
+		assertTrue("Artist expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Artist);
+	}
+
+	@Test
+	public void testPerformSelectQuery2() throws Exception {
+		createArtistsAndPaintingsDataSet();
+
+		// do a query with complex qualifier
+		List<Expression> expressions = new ArrayList<Expression>();
+		expressions.add(ExpressionFactory.matchExp("artistName", "artist3"));
+		expressions.add(ExpressionFactory.matchExp("artistName", "artist5"));
+		expressions.add(ExpressionFactory.matchExp("artistName", "artist21"));
+
+		SelectQuery query = new SelectQuery(Artist.class, ExpressionFactory.joinExp(Expression.OR, expressions));
+
+		List<?> objects = context.performQuery(query);
+
+		assertNotNull(objects);
+		assertEquals(3, objects.size());
+		assertTrue("Artist expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Artist);
+	}
+
+	@Test
+	public void testPerformQuery_Routing() {
+		Query query = mock(Query.class);
+		QueryMetadata md = mock(QueryMetadata.class);
+		when(query.getMetaData(any(EntityResolver.class))).thenReturn(md);
+		context.performGenericQuery(query);
+		verify(query).route(any(QueryRouter.class), eq(context.getEntityResolver()), (Query) isNull());
+	}
+
+	@Test
+	public void testPerformNonSelectingQuery() throws Exception {
+
+		createSingleArtistDataSet();
+
+		SelectQuery select = new SelectQuery(Painting.class, Expression.fromString("db:PAINTING_ID = 1"));
+
+		assertEquals(0, context.performQuery(select).size());
+
+		SQLTemplate query = new SQLTemplate(Painting.class,
+				"INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
+						+ "VALUES (1, 'PX', 33001, 1)");
+		context.performNonSelectingQuery(query);
+		assertEquals(1, context.performQuery(select).size());
+	}
+
+	@Test
+	public void testPerformNonSelectingQueryCounts1() throws Exception {
+		createArtistsDataSet();
+
+		SQLTemplate query = new SQLTemplate(Painting.class,
+				"INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
+						+ "VALUES ($pid, '$pt', $aid, $price)");
+
+		Map<String, Object> map = new HashMap<String, Object>();
+		map.put("pid", new Integer(1));
+		map.put("pt", "P1");
+		map.put("aid", new Integer(33002));
+		map.put("price", new Double(1.1));
+
+		// single batch of parameters
+		query.setParameters(map);
+
+		int[] counts = context.performNonSelectingQuery(query);
+		assertNotNull(counts);
+		assertEquals(1, counts.length);
+		assertEquals(1, counts[0]);
+	}
+
+	@Test
+	public void testPerformNonSelectingQueryCounts2() throws Exception {
+
+		createArtistsDataSet();
+
+		SQLTemplate query = new SQLTemplate(Painting.class,
+				"INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID, ESTIMATED_PRICE) "
+						+ "VALUES ($pid, '$pt', $aid, #bind($price 'DECIMAL' 2))");
+
+		Map<String, Object>[] maps = new Map[3];
+		for (int i = 0; i < maps.length; i++) {
+			maps[i] = new HashMap<String, Object>();
+			maps[i].put("pid", new Integer(1 + i));
+			maps[i].put("pt", "P-" + i);
+			maps[i].put("aid", new Integer(33002));
+			maps[i].put("price", new BigDecimal("1." + i));
+		}
+
+		// single batch of parameters
+		query.setParameters(maps);
+
+		int[] counts = context.performNonSelectingQuery(query);
+		assertNotNull(counts);
+		assertEquals(maps.length, counts.length);
+		for (int i = 0; i < maps.length; i++) {
+			assertEquals(1, counts[i]);
+		}
+
+		SQLTemplate delete = new SQLTemplate(Painting.class, "delete from PAINTING");
+		counts = context.performNonSelectingQuery(delete);
+		assertNotNull(counts);
+		assertEquals(1, counts.length);
+		assertEquals(3, counts[0]);
+	}
+
+	@Test
+	public void testPerformPaginatedQuery() throws Exception {
+		createArtistsDataSet();
+
+		SelectQuery<Artist> query = SelectQuery.query(Artist.class);
+		query.setPageSize(5);
+		List<Artist> objects = context.select(query);
+		assertNotNull(objects);
+		assertTrue(objects instanceof IncrementalFaultList<?>);
+		assertTrue(((IncrementalFaultList<Artist>) objects).elements.get(0) instanceof Long);
+		assertTrue(((IncrementalFaultList<Artist>) objects).elements.get(6) instanceof Long);
+
+		assertTrue(objects.get(0) instanceof Artist);
+	}
+
+	@Test
+	public void testPerformPaginatedQuery1() throws Exception {
+		createArtistsDataSet();
+
+		EJBQLQuery query = new EJBQLQuery("select a FROM Artist a");
+		query.setPageSize(5);
+		List<?> objects = context.performQuery(query);
+		assertNotNull(objects);
+		assertTrue(objects instanceof IncrementalFaultList<?>);
+		assertTrue(((IncrementalFaultList<?>) objects).elements.get(0) instanceof Long);
+		assertTrue(((IncrementalFaultList<?>) objects).elements.get(6) instanceof Long);
+
+		assertTrue(objects.get(0) instanceof Artist);
+	}
+
+	@Test
+	public void testPerformPaginatedQueryBigPage() throws Exception {
+		createArtistsDataSet();
+
+		SelectQuery query = new SelectQuery(Artist.class);
+		query.setPageSize(5);
+		final List<?> objects = context.performQuery(query);
+		assertNotNull(objects);
+		assertTrue(objects instanceof IncrementalFaultList<?>);
+
+		queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
+
+			public void execute() {
+				assertEquals(7, objects.size());
+			}
+		});
+	}
+
+	@Test
+	public void testPerformDataRowQuery() throws Exception {
+
+		createArtistsDataSet();
+
+		SelectQuery query = new SelectQuery(Artist.class);
+		query.setFetchingDataRows(true);
+		List<?> objects = context.performQuery(query);
+
+		assertNotNull(objects);
+		assertEquals(7, objects.size());
+		assertTrue("Map expected, got " + objects.get(0).getClass(), objects.get(0) instanceof Map<?, ?>);
+	}
+
+	@Test
+	public void testCommitChangesRO1() throws Exception {
+
+		ROArtist a1 = (ROArtist) context.newObject("ROArtist");
+		a1.writePropertyDirectly("artistName", "abc");
+		a1.setPersistenceState(PersistenceState.MODIFIED);
+
+		try {
+			context.commitChanges();
+			fail("Inserting a 'read-only' object must fail.");
+		} catch (Exception ex) {
+			// exception is expected,
+			// must blow on saving new "read-only" object.
+		}
+	}
+
+	@Test
+	public void testCommitChangesRO2() throws Exception {
+		createArtistsDataSet();
+
+		SelectQuery query = new SelectQuery(ROArtist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
+				"artist1"));
+		ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
+		a1.writeProperty(ROArtist.ARTIST_NAME_PROPERTY, "abc");
+
+		try {
+			context.commitChanges();
+			fail("Updating a 'read-only' object must fail.");
+		} catch (Exception ex) {
+			// exception is expected,
+			// must blow on saving new "read-only" object.
+		}
+	}
+
+	@Test
+	public void testCommitChangesRO3() throws Exception {
+
+		createArtistsDataSet();
+
+		SelectQuery query = new SelectQuery(ROArtist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
+				"artist1"));
+		ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
+		context.deleteObjects(a1);
+
+		try {
+			context.commitChanges();
+			fail("Deleting a 'read-only' object must fail.");
+		} catch (Exception ex) {
+			// exception is expected,
+			// must blow on saving new "read-only" object.
+		}
+	}
+
+	@Test
+	public void testCommitChangesRO4() throws Exception {
+		createArtistsDataSet();
+
+		SelectQuery query = new SelectQuery(ROArtist.class, ExpressionFactory.matchExp(Artist.ARTIST_NAME_PROPERTY,
+				"artist1"));
+		ROArtist a1 = (ROArtist) context.performQuery(query).get(0);
+
+		Painting painting = context.newObject(Painting.class);
+		painting.setPaintingTitle("paint");
+		a1.addToPaintingArray(painting);
+
+		assertEquals(PersistenceState.MODIFIED, a1.getPersistenceState());
+		try {
+			context.commitChanges();
+		} catch (Exception ex) {
+			fail("Updating 'read-only' object's to-many must succeed, instead an exception was thrown: " + ex);
+		}
+
+		assertEquals(PersistenceState.COMMITTED, a1.getPersistenceState());
+	}
+
+	@Test
 	public void testIterate() throws Exception {
 
 		createArtistsDataSet();
 
 		SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
-		
+
 		final int[] count = new int[1];
-		
+
 		context.iterate(q1, new ResultIteratorCallback<Artist>() {
 
 			@Override
@@ -636,235 +635,225 @@ public class DataContextIT extends ServerCase {
 				count[0]++;
 			}
 		});
-		
-		 assertEquals(7, count[0]);
+
+		assertEquals(7, count[0]);
+	}
+
+	@Test
+	public void testIterateDataRows() throws Exception {
+
+		createArtistsDataSet();
+
+		SelectQuery<DataRow> q1 = SelectQuery.dataRowQuery(Artist.class, null);
+		final int[] count = new int[1];
+
+		context.iterate(q1, new ResultIteratorCallback<DataRow>() {
+
+			@Override
+			public void next(DataRow object) {
+				assertNotNull(object.get("ARTIST_ID"));
+				count[0]++;
+			}
+		});
+
+		assertEquals(7, count[0]);
+	}
+
+	@Test
+	public void testIterator() throws Exception {
+
+		createArtistsDataSet();
+
+		SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
+
+		ResultIterator<Artist> it = context.iterator(q1);
+		try {
+			int count = 0;
+
+			for (Artist a : it) {
+				count++;
+			}
+
+			assertEquals(7, count);
+		} finally {
+			it.close();
+		}
+	}
+
+	@Test
+	public void testBatchIterator() throws Exception {
+		createLargeArtistsDataSet();
+
+		SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
+		ResultBatchIterator<Artist> it = context.batchIterator(q1, 5);
+
+		try {
+			int count = 0;
+
+			for (List<Artist> artistList : it) {
+				count++;
+				assertEquals(5, artistList.size());
+			}
+
+			assertEquals(4, count);
+		} finally {
+			it.close();
+		}
+	}
+
+	@Test
+	public void testPerformIteratedQuery1() throws Exception {
+
+		createArtistsDataSet();
+
+		SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
+		ResultIterator<?> it = context.performIteratedQuery(q1);
+
+		try {
+			int count = 0;
+			while (it.hasNextRow()) {
+				it.nextRow();
+				count++;
+			}
+
+			assertEquals(7, count);
+		} finally {
+			it.close();
+		}
+	}
+
+	@Test
+	public void testPerformIteratedQuery2() throws Exception {
+		createArtistsAndPaintingsDataSet();
+
+		ResultIterator<?> it = context.performIteratedQuery(SelectQuery.query(Artist.class));
+
+		try {
+			while (it.hasNextRow()) {
+				DataRow row = (DataRow) it.nextRow();
+
+				// try instantiating an object and fetching its relationships
+				Artist artist = context.objectFromDataRow(Artist.class, row);
+				List<?> paintings = artist.getPaintingArray();
+				assertNotNull(paintings);
+				assertEquals("Expected one painting for artist: " + artist, 1, paintings.size());
+			}
+		} finally {
+			it.close();
+		}
+	}
+
+	/**
+	 * Tests that hasChanges performs correctly when an object is "modified" and
+	 * the property is simply set to the same value (an unreal modification)
+	 */
+	@Test
+	public void testHasChangesPhantom() {
+
+		String artistName = "ArtistName";
+		Artist artist = (Artist) context.newObject("Artist");
+		artist.setArtistName(artistName);
+		context.commitChanges();
+
+		// Set again to *exactly* the same value
+		artist.setArtistName(artistName);
+
+		// note that since 1.2 the polciy is for hasChanges to return true for
+		// phantom
+		// modifications, as there is no way to detect some more subtle
+		// modifications like
+		// a change of the master related object, until we actually create the
+		// PKs
+		assertTrue(context.hasChanges());
+	}
+
+	/**
+	 * Tests that hasChanges performs correctly when an object is "modified" and
+	 * the property is simply set to the same value (an unreal modification)
+	 */
+	@Test
+	public void testHasChangesRealModify() {
+		Artist artist = (Artist) context.newObject("Artist");
+		artist.setArtistName("ArtistName");
+		context.commitChanges();
+
+		artist.setArtistName("Something different");
+		assertTrue(context.hasChanges());
+	}
+
+	@Test
+	public void testInvalidateObjects_Vararg() throws Exception {
+
+		DataRow row = new DataRow(10);
+		row.put("ARTIST_ID", new Integer(1));
+		row.put("ARTIST_NAME", "ArtistXYZ");
+		row.put("DATE_OF_BIRTH", new Date());
+		DataObject object = context.objectFromDataRow(Artist.class, row);
+		ObjectId oid = object.getObjectId();
+
+		// insert object into the ObjectStore
+		context.getObjectStore().registerNode(oid, object);
+
+		assertSame(object, context.getObjectStore().getNode(oid));
+		assertNotNull(context.getObjectStore().getCachedSnapshot(oid));
+
+		context.invalidateObjects(object);
+
+		assertSame(oid, object.getObjectId());
+		assertNull(context.getObjectStore().getCachedSnapshot(oid));
+		assertSame(object, context.getObjectStore().getNode(oid));
+	}
+
+	@Test
+	public void testInvalidateObjects() throws Exception {
+
+		DataRow row = new DataRow(10);
+		row.put("ARTIST_ID", new Integer(1));
+		row.put("ARTIST_NAME", "ArtistXYZ");
+		row.put("DATE_OF_BIRTH", new Date());
+		DataObject object = context.objectFromDataRow(Artist.class, row);
+		ObjectId oid = object.getObjectId();
+
+		// insert object into the ObjectStore
+		context.getObjectStore().registerNode(oid, object);
+
+		assertSame(object, context.getObjectStore().getNode(oid));
+		assertNotNull(context.getObjectStore().getCachedSnapshot(oid));
+
+		context.invalidateObjects(Collections.singleton(object));
+
+		assertSame(oid, object.getObjectId());
+		assertNull(context.getObjectStore().getCachedSnapshot(oid));
+		assertSame(object, context.getObjectStore().getNode(oid));
+	}
+
+	@Test
+	public void testBeforeHollowDeleteShouldChangeStateToCommited() throws Exception {
+		createSingleArtistDataSet();
+
+		Artist hollow = Cayenne.objectForPK(context, Artist.class, 33001);
+		context.invalidateObjects(hollow);
+		assertEquals(PersistenceState.HOLLOW, hollow.getPersistenceState());
+
+		// testing this...
+		context.deleteObjects(hollow);
+		assertSame(hollow, context.getGraphManager().getNode(new ObjectId("Artist", "ARTIST_ID", 33001)));
+		assertEquals("artist1", hollow.getArtistName());
+
+		assertEquals(PersistenceState.DELETED, hollow.getPersistenceState());
+	}
+
+	@Test
+	public void testCommitUnchangedInsert() throws Exception {
+
+		// see CAY-1444 - reproducible on DB's that support auto incremented PK
+
+		NullTestEntity newObject = context.newObject(NullTestEntity.class);
+
+		assertTrue(context.hasChanges());
+		context.commitChanges();
+		assertFalse(context.hasChanges());
+
+		assertEquals(PersistenceState.COMMITTED, newObject.getPersistenceState());
 	}
-
-    @Test
-    public void testIterateDataRows() throws Exception {
-
-        createArtistsDataSet();
-
-        SelectQuery<DataRow> q1 = SelectQuery.dataRowQuery(Artist.class, null);
-        final int[] count = new int[1];
-        
-        context.iterate(q1, new ResultIteratorCallback<DataRow>() {
-        	
-        	@Override
-        	public void next(DataRow object) {
-        		 assertNotNull(object.get("ARTIST_ID"));
-                 count[0]++;
-        	}
-        });
-        
-        assertEquals(7, count[0]);
-    }
-
-    @Test
-    public void testIterator() throws Exception {
-
-        createArtistsDataSet();
-
-        SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
-
-        ResultIterator<Artist> it = context.iterator(q1);
-        try {
-            int count = 0;
-
-            for (Artist a : it) {
-                count++;
-            }
-
-            assertEquals(7, count);
-        } finally {
-            it.close();
-        }
-    }
-
-    @Test
-    public void testBatchIterator() throws Exception {
-        createLargeArtistsDataSet();
-
-        SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
-        ResultBatchIterator<Artist> it = context.batchIterator(q1, 5);
-
-        try {
-            int count = 0;
-
-            for (List<Artist> artistList : it) {
-                count++;
-                assertEquals(5, artistList.size());
-            }
-
-            assertEquals(4, count);
-        } finally {
-            it.close();
-        }
-    }
-
-    @Test
-    public void testPerformIteratedQuery1() throws Exception {
-
-        createArtistsDataSet();
-
-        SelectQuery<Artist> q1 = new SelectQuery<Artist>(Artist.class);
-        ResultIterator<?> it = context.performIteratedQuery(q1);
-
-        try {
-            int count = 0;
-            while (it.hasNextRow()) {
-                it.nextRow();
-                count++;
-            }
-
-            assertEquals(7, count);
-        } finally {
-            it.close();
-        }
-    }
-
-    @Test
-    public void testPerformIteratedQuery2() throws Exception {
-        createArtistsAndPaintingsDataSet();
-
-        ResultIterator it = context.performIteratedQuery(new SelectQuery(Artist.class));
-
-        // just for this test increase pool size
-        changeMaxConnections(1);
-
-        try {
-            while (it.hasNextRow()) {
-                DataRow row = (DataRow) it.nextRow();
-
-                // try instantiating an object and fetching its relationships
-                Artist artist = context.objectFromDataRow(Artist.class, row);
-                List<?> paintings = artist.getPaintingArray();
-                assertNotNull(paintings);
-                assertEquals("Expected one painting for artist: " + artist, 1, paintings.size());
-            }
-        } finally {
-            // change allowed connections back
-            changeMaxConnections(-1);
-            it.close();
-        }
-    }
-
-    /**
-     * Tests that hasChanges performs correctly when an object is "modified" and
-     * the property is simply set to the same value (an unreal modification)
-     */
-    @Test
-    public void testHasChangesPhantom() {
-
-        String artistName = "ArtistName";
-        Artist artist = (Artist) context.newObject("Artist");
-        artist.setArtistName(artistName);
-        context.commitChanges();
-
-        // Set again to *exactly* the same value
-        artist.setArtistName(artistName);
-
-        // note that since 1.2 the polciy is for hasChanges to return true for
-        // phantom
-        // modifications, as there is no way to detect some more subtle
-        // modifications like
-        // a change of the master related object, until we actually create the
-        // PKs
-        assertTrue(context.hasChanges());
-    }
-
-    /**
-     * Tests that hasChanges performs correctly when an object is "modified" and
-     * the property is simply set to the same value (an unreal modification)
-     */
-    @Test
-    public void testHasChangesRealModify() {
-        Artist artist = (Artist) context.newObject("Artist");
-        artist.setArtistName("ArtistName");
-        context.commitChanges();
-
-        artist.setArtistName("Something different");
-        assertTrue(context.hasChanges());
-    }
-
-    @Test
-    public void testInvalidateObjects_Vararg() throws Exception {
-
-        DataRow row = new DataRow(10);
-        row.put("ARTIST_ID", new Integer(1));
-        row.put("ARTIST_NAME", "ArtistXYZ");
-        row.put("DATE_OF_BIRTH", new Date());
-        DataObject object = context.objectFromDataRow(Artist.class, row);
-        ObjectId oid = object.getObjectId();
-
-        // insert object into the ObjectStore
-        context.getObjectStore().registerNode(oid, object);
-
-        assertSame(object, context.getObjectStore().getNode(oid));
-        assertNotNull(context.getObjectStore().getCachedSnapshot(oid));
-
-        context.invalidateObjects(object);
-
-        assertSame(oid, object.getObjectId());
-        assertNull(context.getObjectStore().getCachedSnapshot(oid));
-        assertSame(object, context.getObjectStore().getNode(oid));
-    }
-
-    @Test
-    public void testInvalidateObjects() throws Exception {
-
-        DataRow row = new DataRow(10);
-        row.put("ARTIST_ID", new Integer(1));
-        row.put("ARTIST_NAME", "ArtistXYZ");
-        row.put("DATE_OF_BIRTH", new Date());
-        DataObject object = context.objectFromDataRow(Artist.class, row);
-        ObjectId oid = object.getObjectId();
-
-        // insert object into the ObjectStore
-        context.getObjectStore().registerNode(oid, object);
-
-        assertSame(object, context.getObjectStore().getNode(oid));
-        assertNotNull(context.getObjectStore().getCachedSnapshot(oid));
-
-        context.invalidateObjects(Collections.singleton(object));
-
-        assertSame(oid, object.getObjectId());
-        assertNull(context.getObjectStore().getCachedSnapshot(oid));
-        assertSame(object, context.getObjectStore().getNode(oid));
-    }
-
-    @Test
-    public void testBeforeHollowDeleteShouldChangeStateToCommited() throws Exception {
-        createSingleArtistDataSet();
-
-        Artist hollow = Cayenne.objectForPK(context, Artist.class, 33001);
-        context.invalidateObjects(hollow);
-        assertEquals(PersistenceState.HOLLOW, hollow.getPersistenceState());
-
-        // testing this...
-        context.deleteObjects(hollow);
-        assertSame(hollow, context.getGraphManager().getNode(new ObjectId("Artist", "ARTIST_ID", 33001)));
-        assertEquals("artist1", hollow.getArtistName());
-
-        assertEquals(PersistenceState.DELETED, hollow.getPersistenceState());
-    }
-
-    @Test
-    public void testCommitUnchangedInsert() throws Exception {
-
-        // see CAY-1444 - reproducible on DB's that support auto incremented PK
-
-        NullTestEntity newObject = context.newObject(NullTestEntity.class);
-
-        assertTrue(context.hasChanges());
-        context.commitChanges();
-        assertFalse(context.hasChanges());
-
-        assertEquals(PersistenceState.COMMITTED, newObject.getPersistenceState());
-    }
-
-    private void changeMaxConnections(int delta) {
-        PoolManager manager = (PoolManager) dataSourceFactory.getSharedDataSource();
-        manager.setMaxConnections(manager.getMaxConnections() + delta);
-    }
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/conn/BasePoolingDataSourceIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/conn/BasePoolingDataSourceIT.java b/cayenne-server/src/test/java/org/apache/cayenne/conn/BasePoolingDataSourceIT.java
new file mode 100644
index 0000000..ef81d1a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/conn/BasePoolingDataSourceIT.java
@@ -0,0 +1,74 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.conn;
+
+import java.sql.Driver;
+import java.sql.SQLException;
+
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
+import org.junit.Before;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class BasePoolingDataSourceIT extends ServerCase {
+
+	@Inject
+	private DataSourceInfo dataSourceInfo;
+
+	@Inject
+	private AdhocObjectFactory objectFactory;
+
+	@Inject
+	private JdbcEventLogger logger;
+
+	protected PoolingDataSource dataSource;
+
+	@Before
+	public void before() throws SQLException {
+
+		Driver driver = objectFactory.newInstance(Driver.class, dataSourceInfo.getJdbcDriver());
+		DriverDataSource nonPooling = new DriverDataSource(driver, dataSourceInfo.getDataSourceUrl(),
+				dataSourceInfo.getUserName(), dataSourceInfo.getPassword());
+		nonPooling.setLogger(logger);
+
+		PoolingDataSourceParameters poolParameters = createParameters();
+		this.dataSource = new PoolingDataSource(new PooledConnectionFactory(nonPooling), poolParameters);
+	}
+
+	@After
+	public void after() throws SQLException {
+		if (dataSource != null) {
+			dataSource.beforeScopeEnd();
+		}
+	}
+
+	protected PoolingDataSourceParameters createParameters() {
+		PoolingDataSourceParameters poolParameters = new PoolingDataSourceParameters();
+		poolParameters.setMinConnections(2);
+		poolParameters.setMaxConnections(3);
+		poolParameters.setMaxQueueWaitTime(1000);
+		return poolParameters;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolManagerIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolManagerIT.java b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolManagerIT.java
deleted file mode 100644
index 832470b..0000000
--- a/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolManagerIT.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.conn;
-
-import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.unit.di.server.CayenneProjects;
-import org.apache.cayenne.unit.di.server.ServerCase;
-import org.apache.cayenne.unit.di.server.UseServerRuntime;
-import org.junit.Test;
-
-import java.sql.Connection;
-
-import static org.junit.Assert.assertEquals;
-
-@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
-public class PoolManagerIT extends ServerCase {
-
-    @Inject
-    private DataSourceInfo dataSourceInfo;
-
-    @Test
-    public void testDataSourceUrl() throws Exception {
-        String driverName = dataSourceInfo.getJdbcDriver();
-        String url = dataSourceInfo.getDataSourceUrl();
-
-        PoolManager pm = new PoolManager(driverName, url, 0, 3, "", "") {
-
-            @Override
-            protected void startMaintenanceThread() {
-            }
-        };
-        assertEquals(url, pm.getDataSourceUrl());
-        assertEquals(driverName, pm.getJdbcDriver());
-    }
-
-    @Test
-    public void testPassword() throws Exception {
-        PoolManager pm = new PoolManager(null, 0, 3, "", "b") {
-
-            @Override
-            protected void startMaintenanceThread() {
-            }
-        };
-        assertEquals("b", pm.getPassword());
-    }
-
-    @Test
-    public void testUserName() throws Exception {
-        PoolManager pm = new PoolManager(null, 0, 3, "a", "") {
-
-            @Override
-            protected void startMaintenanceThread() {
-            }
-        };
-        assertEquals("a", pm.getUserName());
-    }
-
-    @Test
-    public void testMinConnections() throws Exception {
-        PoolManager pm = new PoolManager(null, 0, 3, "", "") {
-
-            @Override
-            protected void startMaintenanceThread() {
-            }
-        };
-        assertEquals(0, pm.getMinConnections());
-    }
-
-    @Test
-    public void testMaxConnections() throws Exception {
-        PoolManager pm = new PoolManager(null, 0, 3, "", "") {
-
-            @Override
-            protected void startMaintenanceThread() {
-            }
-        };
-        assertEquals(3, pm.getMaxConnections());
-    }
-
-    @Test
-    public void testPooling() throws Exception {
-
-        PoolManager pm = new PoolManager(dataSourceInfo.getJdbcDriver(), dataSourceInfo
-                .getDataSourceUrl(), 2, 3, dataSourceInfo.getUserName(), dataSourceInfo
-                .getPassword());
-
-        try {
-            assertEquals(0, pm.getCurrentlyInUse());
-            assertEquals(2, pm.getCurrentlyUnused());
-
-            Connection c1 = pm.getConnection();
-            assertEquals(1, pm.getCurrentlyInUse());
-            assertEquals(1, pm.getCurrentlyUnused());
-
-            Connection c2 = pm.getConnection();
-            assertEquals(2, pm.getCurrentlyInUse());
-            assertEquals(0, pm.getCurrentlyUnused());
-
-            c1.close();
-            assertEquals(1, pm.getCurrentlyInUse());
-            assertEquals(1, pm.getCurrentlyUnused());
-
-            c2.close();
-            assertEquals(0, pm.getCurrentlyInUse());
-            assertEquals(2, pm.getCurrentlyUnused());
-        }
-        finally {
-            pm.shutdown();
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/conn/PooledConnectionImplTest.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/conn/PooledConnectionImplTest.java b/cayenne-server/src/test/java/org/apache/cayenne/conn/PooledConnectionImplTest.java
index e829372..829ceb6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/conn/PooledConnectionImplTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/conn/PooledConnectionImplTest.java
@@ -19,35 +19,49 @@
 
 package org.apache.cayenne.conn;
 
+import static org.mockito.Mockito.mock;
+
+import java.sql.SQLException;
+
+import javax.sql.ConnectionPoolDataSource;
+import javax.sql.DataSource;
+
+import org.junit.Before;
 import org.junit.Test;
 
 public class PooledConnectionImplTest {
 
-    @Test
-    public void testConnectionErrorNotificationConcurrency() throws Exception {
-        // test a case when error notification is sent to connection
-        // that has been removed from the pool, but when pool is still a 
-        // listener for its events.
-        PoolManager pm = new PoolManager(null, 0, 3, "", "") {
-            @Override
-            protected void startMaintenanceThread() {}
-        };
-        PooledConnectionImpl con = new PooledConnectionImpl();
-        con.addConnectionEventListener(pm);
-        con.connectionErrorNotification(new java.sql.SQLException("Bad SQL Exception.."));
-    }
-
-    @Test
-    public void testConnectionClosedNotificationConcurrency() throws Exception {
-        // test a case when closed notification is sent to connection
-        // that has been removed from the pool, but when pool is still a 
-        // listener for its events.
-        PoolManager pm = new PoolManager(null, 0, 3, "", "") {
-            @Override
-            protected void startMaintenanceThread() {}
-        };
-        PooledConnectionImpl con = new PooledConnectionImpl();
-        con.addConnectionEventListener(pm);
-        con.connectionClosedNotification();
-    }
+	private PoolingDataSource dataSource;
+
+	@Before
+	public void before() throws SQLException {
+		PoolingDataSourceParameters poolParameters = new PoolingDataSourceParameters();
+		poolParameters.setMinConnections(0);
+		poolParameters.setMaxConnections(0);
+		poolParameters.setMaxQueueWaitTime(1000);
+
+		this.dataSource = new PoolingDataSource(mock(ConnectionPoolDataSource.class), poolParameters);
+	}
+
+	@Test
+	public void testConnectionErrorNotificationConcurrency() throws Exception {
+		// test a case when error notification is sent to connection
+		// that has been removed from the pool, but when pool is still a
+		// listener for its events.
+
+		PooledConnectionImpl con = new PooledConnectionImpl(mock(DataSource.class), "un", "password");
+		con.addConnectionEventListener(dataSource);
+		con.connectionErrorNotification(new java.sql.SQLException("Bad SQL Exception.."));
+	}
+
+	@Test
+	public void testConnectionClosedNotificationConcurrency() throws Exception {
+		// test a case when closed notification is sent to connection
+		// that has been removed from the pool, but when pool is still a
+		// listener for its events.
+		PooledConnectionImpl con = new PooledConnectionImpl(mock(DataSource.class), "un", "password");
+		con.addConnectionEventListener(dataSource);
+		con.connectionClosedNotification();
+	}
+
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSourceIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSourceIT.java b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSourceIT.java
new file mode 100644
index 0000000..d2814d9
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSourceIT.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.conn;
+
+import static org.junit.Assert.assertEquals;
+
+import java.sql.Connection;
+
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class PoolingDataSourceIT extends BasePoolingDataSourceIT {
+
+	@Test(expected = UnsupportedOperationException.class)
+	public void testGetConnectionWithUserName() throws Exception {
+		dataSource.getConnection("user", "password");
+	}
+
+	@Test
+	public void testGetConnection() throws Exception {
+
+		assertEquals(0, dataSource.getCurrentlyInUse());
+		assertEquals(2, dataSource.getCurrentlyUnused());
+
+		Connection c1 = dataSource.getConnection();
+		assertEquals(1, dataSource.getCurrentlyInUse());
+		assertEquals(1, dataSource.getCurrentlyUnused());
+
+		Connection c2 = dataSource.getConnection();
+		assertEquals(2, dataSource.getCurrentlyInUse());
+		assertEquals(0, dataSource.getCurrentlyUnused());
+
+		c1.close();
+		assertEquals(1, dataSource.getCurrentlyInUse());
+		assertEquals(1, dataSource.getCurrentlyUnused());
+
+		c2.close();
+		assertEquals(0, dataSource.getCurrentlyInUse());
+		assertEquals(2, dataSource.getCurrentlyUnused());
+	}
+
+	@Test
+	public void testGetConnection_BeforeScopeEnd() throws Exception {
+
+		assertEquals(0, dataSource.getCurrentlyInUse());
+		assertEquals(2, dataSource.getCurrentlyUnused());
+
+		dataSource.beforeScopeEnd();
+
+		assertEquals(0, dataSource.getCurrentlyInUse());
+		assertEquals(0, dataSource.getCurrentlyUnused());
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_FailingValidationQueryIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_FailingValidationQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_FailingValidationQueryIT.java
new file mode 100644
index 0000000..d832ae5
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_FailingValidationQueryIT.java
@@ -0,0 +1,43 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.conn;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class PoolingDataSource_FailingValidationQueryIT extends BasePoolingDataSourceIT {
+
+	@Override
+	protected PoolingDataSourceParameters createParameters() {
+		PoolingDataSourceParameters params = super.createParameters();
+		params.setValidationQuery("SELECT count(1) FROM NO_SUCH_TABLE");
+		return params;
+	}
+
+	@Test(expected = SQLException.class)
+	public void testGetConnection_ValidationQuery() throws Exception {
+		Connection c1 = dataSource.getConnection();
+		c1.close();
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_ValidationQueryIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_ValidationQueryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_ValidationQueryIT.java
new file mode 100644
index 0000000..45a5689
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/conn/PoolingDataSource_ValidationQueryIT.java
@@ -0,0 +1,55 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.conn;
+
+import static org.junit.Assert.assertEquals;
+
+import java.sql.Connection;
+
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class PoolingDataSource_ValidationQueryIT extends BasePoolingDataSourceIT {
+
+	@Override
+	protected PoolingDataSourceParameters createParameters() {
+		PoolingDataSourceParameters params = super.createParameters();
+		params.setValidationQuery("SELECT count(1) FROM ARTIST");
+		return params;
+	}
+
+	@Test
+	public void testGetConnection_ValidationQuery() throws Exception {
+
+		assertEquals(0, dataSource.getCurrentlyInUse());
+		assertEquals(2, dataSource.getCurrentlyUnused());
+
+		// TODO: we are not testing much here... we really need to mock
+		// validation query execution somehow and verify that it is taken into
+		// account
+		
+		Connection c1 = dataSource.getConnection();
+		assertEquals(1, dataSource.getCurrentlyInUse());
+		assertEquals(1, dataSource.getCurrentlyUnused());
+
+		c1.close();
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceFactory.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceFactory.java
index 6859f2f..43ef5f3 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceFactory.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceFactory.java
@@ -18,7 +18,6 @@
  ****************************************************************/
 package org.apache.cayenne.unit.di.server;
 
-import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -27,70 +26,54 @@ import java.util.Set;
 
 import javax.sql.DataSource;
 
+import org.apache.cayenne.conn.DataSourceBuilder;
 import org.apache.cayenne.conn.DataSourceInfo;
-import org.apache.cayenne.conn.PoolDataSource;
-import org.apache.cayenne.conn.PoolManager;
+import org.apache.cayenne.di.AdhocObjectFactory;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.log.JdbcEventLogger;
 
-public class ServerCaseDataSourceFactory  {
+public class ServerCaseDataSourceFactory {
 
-    private DataSource sharedDataSource;
-    private DataSourceInfo dataSourceInfo;
-    private Map<String, DataSource> dataSources;
-    private Set<String> mapsWithDedicatedDataSource;
+	private DataSource sharedDataSource;
+	private DataSourceInfo dataSourceInfo;
+	private Map<String, DataSource> dataSources;
+	private Set<String> mapsWithDedicatedDataSource;
+	private AdhocObjectFactory objectFactory;
+	private JdbcEventLogger logger;
 
-    public ServerCaseDataSourceFactory(@Inject DataSourceInfo dataSourceInfo) {
+	public ServerCaseDataSourceFactory(@Inject DataSourceInfo dataSourceInfo, @Inject AdhocObjectFactory objectFactory,
+			@Inject JdbcEventLogger logger) {
 
-        this.dataSourceInfo = dataSourceInfo;
-        this.dataSources = new HashMap<String, DataSource>();
-        this.mapsWithDedicatedDataSource = new HashSet<String>(Arrays.asList(
-                "map-db1",
-                "map-db2"));
+		this.logger = logger;
+		this.objectFactory = objectFactory;
+		this.dataSourceInfo = dataSourceInfo;
+		this.dataSources = new HashMap<String, DataSource>();
+		this.mapsWithDedicatedDataSource = new HashSet<String>(Arrays.asList("map-db1", "map-db2"));
 
-        this.sharedDataSource = createDataSource();
-    }
+		this.sharedDataSource = createDataSource();
+	}
 
-    public DataSource getSharedDataSource() {
-        return sharedDataSource;
-    }
+	public DataSource getSharedDataSource() {
+		return sharedDataSource;
+	}
 
-    public DataSource getDataSource(String dataMapName) {
-        DataSource ds = dataSources.get(dataMapName);
-        if (ds == null) {
+	public DataSource getDataSource(String dataMapName) {
+		DataSource ds = dataSources.get(dataMapName);
+		if (ds == null) {
 
-            ds = mapsWithDedicatedDataSource.contains(dataMapName)
-                    ? createDataSource()
-                    : sharedDataSource;
+			ds = mapsWithDedicatedDataSource.contains(dataMapName) ? createDataSource() : sharedDataSource;
 
-            dataSources.put(dataMapName, ds);
-        }
+			dataSources.put(dataMapName, ds);
+		}
 
-        return ds;
-    }
+		return ds;
+	}
 
-    private DataSource createDataSource() {
-        try {
-            PoolDataSource poolDS = new PoolDataSource(
-                    dataSourceInfo.getJdbcDriver(),
-                    dataSourceInfo.getDataSourceUrl());
-            return new PoolManager(
-                    poolDS,
-                    dataSourceInfo.getMinConnections(),
-                    dataSourceInfo.getMaxConnections(),
-                    dataSourceInfo.getUserName(),
-                    dataSourceInfo.getPassword(), PoolManager.MAX_QUEUE_WAIT_DEFAULT) {
-
-                @Override
-                public void shutdown() throws SQLException {
-                    // noop - make sure we are not shutdown by the test scope, but at the
-                    // same time PoolManager methods are exposed (so we can't wrap
-                    // PoolManager)
-                }
-            };
-        }
-        catch (Exception ex) {
-            throw new RuntimeException("Can not create shared data source.", ex);
-        }
-    }
+	private DataSource createDataSource() {
+		return DataSourceBuilder.builder(objectFactory, logger).driver(dataSourceInfo.getJdbcDriver())
+				.url(dataSourceInfo.getDataSourceUrl()).userName(dataSourceInfo.getUserName())
+				.password(dataSourceInfo.getPassword()).minConnections(dataSourceInfo.getMinConnections())
+				.maxConnections(dataSourceInfo.getMaxConnections()).build();
+	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/docs/doc/src/main/resources/RELEASE-NOTES.txt
----------------------------------------------------------------------
diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt
index 2d270aa..fb1d0a8 100644
--- a/docs/doc/src/main/resources/RELEASE-NOTES.txt
+++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt
@@ -21,6 +21,7 @@ CAY-2001 Saving a display state of Project
 CAY-2004 EJBQL: Support for ordering on aggregate expressions
 CAY-2005 Improving generated DISTINCT performance on PostgreSQL
 CAY-2007 Refactoring SelectTranslator for better extensibility
+CAY-2008 Connection pool refactoring and validation query support in Cayenne DataSource
 
 Bug Fixes:
 


[2/2] cayenne git commit: CAY-2008 Connection pool refactoring and validation query support in Cayenne DataSource

Posted by aa...@apache.org.
CAY-2008 Connection pool refactoring and validation query support in Cayenne DataSource

* Refactoring and cleanup of DataSource implementation
* DataSourceBuilder
* splitting pool management thread into a separate DS wrapper
* implementing java.sql.Wrapper methods
* pool specific parameters are passed as a special object
* stop supporting getting connections with expclicit username/password. We do not have any
  notion of per-user connection pools, so it is only misleading and can lead
  to unpredictable pool state
* validate query


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/8b54c052
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/8b54c052
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/8b54c052

Branch: refs/heads/master
Commit: 8b54c052d6e54a5218c049819e35d718b4049af0
Parents: 5e037fc
Author: aadamchik <aa...@apache.org>
Authored: Thu Apr 30 13:34:21 2015 +0300
Committer: aadamchik <aa...@apache.org>
Committed: Thu Apr 30 18:46:25 2015 +0300

----------------------------------------------------------------------
 .../apache/cayenne/di/ScopeEventListener.java   |   19 +-
 .../server/PropertyDataSourceFactory.java       |   86 +-
 .../server/XMLPoolingDataSourceFactory.java     |   59 +-
 .../apache/cayenne/conn/DataSourceBuilder.java  |  135 ++
 .../org/apache/cayenne/conn/DataSourceInfo.java |   74 +-
 .../cayenne/conn/ManagedPoolingDataSource.java  |  155 ++
 .../org/apache/cayenne/conn/PoolDataSource.java |   90 -
 .../org/apache/cayenne/conn/PoolManager.java    |  649 --------
 .../cayenne/conn/PooledConnectionFactory.java   |   90 +
 .../cayenne/conn/PooledConnectionImpl.java      |  320 ++--
 .../apache/cayenne/conn/PoolingDataSource.java  |  471 ++++++
 .../conn/PoolingDataSourceParameters.java       |   64 +
 .../apache/cayenne/access/DataContextIT.java    | 1571 +++++++++---------
 .../cayenne/conn/BasePoolingDataSourceIT.java   |   74 +
 .../org/apache/cayenne/conn/PoolManagerIT.java  |  128 --
 .../cayenne/conn/PooledConnectionImplTest.java  |   68 +-
 .../cayenne/conn/PoolingDataSourceIT.java       |   72 +
 ...lingDataSource_FailingValidationQueryIT.java |   43 +
 .../PoolingDataSource_ValidationQueryIT.java    |   55 +
 .../di/server/ServerCaseDataSourceFactory.java  |   89 +-
 docs/doc/src/main/resources/RELEASE-NOTES.txt   |    1 +
 21 files changed, 2296 insertions(+), 2017 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-di/src/main/java/org/apache/cayenne/di/ScopeEventListener.java
----------------------------------------------------------------------
diff --git a/cayenne-di/src/main/java/org/apache/cayenne/di/ScopeEventListener.java b/cayenne-di/src/main/java/org/apache/cayenne/di/ScopeEventListener.java
index 171bd50..b6959ac 100644
--- a/cayenne-di/src/main/java/org/apache/cayenne/di/ScopeEventListener.java
+++ b/cayenne-di/src/main/java/org/apache/cayenne/di/ScopeEventListener.java
@@ -18,18 +18,19 @@
  ****************************************************************/
 package org.apache.cayenne.di;
 
-
 /**
- * This interface duplicates default reflection based mechanism for receiving DI events.
- * It is not fully supported and its usage are reserved for cases when for some reason
- * it is not possible to use reflection. It is used for example in
- * {@link javax.sql.DataSource} managing layer to provide compatibility with java version 5.
+ * This interface duplicates default reflection based mechanism for receiving DI
+ * events. It is not fully supported and its usage are reserved for cases when
+ * for some reason it is not possible to use reflection. It is used for example
+ * in {@link javax.sql.DataSource} management layer to provide compatibility
+ * with java version 5.
  *
  * @since 3.1
  */
 public interface ScopeEventListener {
-    /**
-     * Similar to {@link BeforeScopeEnd}
-     */
-    void beforeScopeEnd();
+	
+	/**
+	 * Similar to {@link BeforeScopeEnd}
+	 */
+	void beforeScopeEnd();
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
index 237f4e4..87d18de 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/PropertyDataSourceFactory.java
@@ -18,17 +18,14 @@
  ****************************************************************/
 package org.apache.cayenne.configuration.server;
 
-import java.sql.Driver;
-
 import javax.sql.DataSource;
 
 import org.apache.cayenne.ConfigurationException;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.configuration.RuntimeProperties;
-import org.apache.cayenne.conn.DriverDataSource;
-import org.apache.cayenne.conn.PoolDataSource;
-import org.apache.cayenne.conn.PoolManager;
+import org.apache.cayenne.conn.DataSourceBuilder;
+import org.apache.cayenne.conn.PoolingDataSource;
 import org.apache.cayenne.di.AdhocObjectFactory;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.log.JdbcEventLogger;
@@ -52,57 +49,50 @@ import org.apache.cayenne.log.JdbcEventLogger;
  */
 public class PropertyDataSourceFactory implements DataSourceFactory {
 
-    @Inject
-    protected RuntimeProperties properties;
-
-    @Inject
-    protected JdbcEventLogger jdbcEventLogger;
+	@Inject
+	protected RuntimeProperties properties;
 
-    @Inject
-    private AdhocObjectFactory objectFactory;
+	@Inject
+	protected JdbcEventLogger jdbcEventLogger;
 
-    @Override
-    public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) throws Exception {
+	@Inject
+	private AdhocObjectFactory objectFactory;
 
-        String suffix = "." + nodeDescriptor.getDataChannelDescriptor().getName() + "." + nodeDescriptor.getName();
+	@Override
+	public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) throws Exception {
 
-        String driverClass = getProperty(Constants.JDBC_DRIVER_PROPERTY, suffix);
-        String url = getProperty(Constants.JDBC_URL_PROPERTY, suffix);
-        String username = getProperty(Constants.JDBC_USERNAME_PROPERTY, suffix);
-        String password = getProperty(Constants.JDBC_PASSWORD_PROPERTY, suffix);
-        int minConnections = getIntProperty(Constants.JDBC_MIN_CONNECTIONS_PROPERTY, suffix, 1);
-        int maxConnections = getIntProperty(Constants.JDBC_MAX_CONNECTIONS_PROPERTY, suffix, 1);
+		String suffix = "." + nodeDescriptor.getDataChannelDescriptor().getName() + "." + nodeDescriptor.getName();
 
-        Driver driver = objectFactory.newInstance(Driver.class, driverClass);
-        DriverDataSource driverDS = new DriverDataSource(driver, url, username, password);
-        driverDS.setLogger(jdbcEventLogger);
-        PoolDataSource poolDS = new PoolDataSource(driverDS);
+		String driverClass = getProperty(Constants.JDBC_DRIVER_PROPERTY, suffix);
+		String url = getProperty(Constants.JDBC_URL_PROPERTY, suffix);
+		String username = getProperty(Constants.JDBC_USERNAME_PROPERTY, suffix);
+		String password = getProperty(Constants.JDBC_PASSWORD_PROPERTY, suffix);
+		int minConnections = getIntProperty(Constants.JDBC_MIN_CONNECTIONS_PROPERTY, suffix, 1);
+		int maxConnections = getIntProperty(Constants.JDBC_MAX_CONNECTIONS_PROPERTY, suffix, 1);
+		long maxQueueWaitTime = properties.getLong(Constants.SERVER_MAX_QUEUE_WAIT_TIME,
+				PoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
 
-        try {
-            return new PoolManager(poolDS, minConnections, maxConnections, username, password, properties.getLong(
-                    Constants.SERVER_MAX_QUEUE_WAIT_TIME, PoolManager.MAX_QUEUE_WAIT_DEFAULT));
-        } catch (Exception e) {
-            jdbcEventLogger.logConnectFailure(e);
-            throw e;
-        }
-    }
+		return DataSourceBuilder.builder(objectFactory, jdbcEventLogger).driver(driverClass).url(url)
+				.userName(username).password(password).minConnections(minConnections).maxConnections(maxConnections)
+				.maxQueueWaitTime(maxQueueWaitTime).build();
+	}
 
-    protected int getIntProperty(String propertyName, String suffix, int defaultValue) {
-        String string = getProperty(propertyName, suffix);
+	protected int getIntProperty(String propertyName, String suffix, int defaultValue) {
+		String string = getProperty(propertyName, suffix);
 
-        if (string == null) {
-            return defaultValue;
-        }
+		if (string == null) {
+			return defaultValue;
+		}
 
-        try {
-            return Integer.parseInt(string);
-        } catch (NumberFormatException e) {
-            throw new ConfigurationException("Invalid int property '%s': '%s'", propertyName, string);
-        }
-    }
+		try {
+			return Integer.parseInt(string);
+		} catch (NumberFormatException e) {
+			throw new ConfigurationException("Invalid int property '%s': '%s'", propertyName, string);
+		}
+	}
 
-    protected String getProperty(String propertyName, String suffix) {
-        String value = properties.get(propertyName + suffix);
-        return value != null ? value : properties.get(propertyName);
-    }
+	protected String getProperty(String propertyName, String suffix) {
+		String value = properties.get(propertyName + suffix);
+		return value != null ? value : properties.get(propertyName);
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
index b3bad8d..a62a2d9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/XMLPoolingDataSourceFactory.java
@@ -18,18 +18,15 @@
  ****************************************************************/
 package org.apache.cayenne.configuration.server;
 
-import java.sql.Driver;
-
 import javax.sql.DataSource;
 
 import org.apache.cayenne.ConfigurationException;
 import org.apache.cayenne.configuration.Constants;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.configuration.RuntimeProperties;
+import org.apache.cayenne.conn.DataSourceBuilder;
 import org.apache.cayenne.conn.DataSourceInfo;
-import org.apache.cayenne.conn.DriverDataSource;
-import org.apache.cayenne.conn.PoolDataSource;
-import org.apache.cayenne.conn.PoolManager;
+import org.apache.cayenne.conn.PoolingDataSource;
 import org.apache.cayenne.di.AdhocObjectFactory;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.log.JdbcEventLogger;
@@ -47,43 +44,35 @@ import org.apache.commons.logging.LogFactory;
 // something else?
 public class XMLPoolingDataSourceFactory implements DataSourceFactory {
 
-    private static final Log logger = LogFactory.getLog(XMLPoolingDataSourceFactory.class);
+	private static final Log logger = LogFactory.getLog(XMLPoolingDataSourceFactory.class);
 
-    @Inject
-    protected JdbcEventLogger jdbcEventLogger;
+	@Inject
+	protected JdbcEventLogger jdbcEventLogger;
 
-    @Inject
-    private RuntimeProperties properties;
+	@Inject
+	private RuntimeProperties properties;
 
-    @Inject
-    private AdhocObjectFactory objectFactory;
+	@Inject
+	private AdhocObjectFactory objectFactory;
 
-    @Override
-    public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) throws Exception {
+	@Override
+	public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) throws Exception {
 
-        DataSourceInfo dataSourceDescriptor = nodeDescriptor.getDataSourceDescriptor();
+		DataSourceInfo descriptor = nodeDescriptor.getDataSourceDescriptor();
 
-        if (dataSourceDescriptor == null) {
-            String message = "Null dataSourceDescriptor for nodeDescriptor '" + nodeDescriptor.getName() + "'";
-            logger.info(message);
-            throw new ConfigurationException(message);
-        }
+		if (descriptor == null) {
+			String message = "Null dataSourceDescriptor for nodeDescriptor '" + nodeDescriptor.getName() + "'";
+			logger.info(message);
+			throw new ConfigurationException(message);
+		}
 
-        Driver driver = objectFactory.newInstance(Driver.class, dataSourceDescriptor.getJdbcDriver());
-        DriverDataSource driverDS = new DriverDataSource(driver, dataSourceDescriptor.getDataSourceUrl(),
-                dataSourceDescriptor.getUserName(), dataSourceDescriptor.getPassword());
-        driverDS.setLogger(jdbcEventLogger);
-        PoolDataSource poolDS = new PoolDataSource(driverDS);
+		long maxQueueWaitTime = properties.getLong(Constants.SERVER_MAX_QUEUE_WAIT_TIME,
+				PoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
 
-        try {
-            return new PoolManager(poolDS, dataSourceDescriptor.getMinConnections(),
-                    dataSourceDescriptor.getMaxConnections(), dataSourceDescriptor.getUserName(),
-                    dataSourceDescriptor.getPassword(), properties.getLong(Constants.SERVER_MAX_QUEUE_WAIT_TIME,
-                            PoolManager.MAX_QUEUE_WAIT_DEFAULT));
-        } catch (Exception e) {
-            jdbcEventLogger.logConnectFailure(e);
-            throw e;
-        }
-    }
+		return DataSourceBuilder.builder(objectFactory, jdbcEventLogger).driver(descriptor.getJdbcDriver())
+				.url(descriptor.getDataSourceUrl()).userName(descriptor.getUserName())
+				.password(descriptor.getPassword()).minConnections(descriptor.getMinConnections())
+				.maxConnections(descriptor.getMaxConnections()).maxQueueWaitTime(maxQueueWaitTime).build();
+	}
 
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceBuilder.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceBuilder.java
new file mode 100644
index 0000000..c850d64
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceBuilder.java
@@ -0,0 +1,135 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.conn;
+
+import java.sql.Driver;
+
+import javax.sql.DataSource;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.log.JdbcEventLogger;
+import org.apache.cayenne.log.NoopJdbcEventLogger;
+
+/**
+ * A builder class that creates a default Cayenne implementation of a pooling
+ * {@link DataSource}.
+ * 
+ * @since 4.0
+ */
+public class DataSourceBuilder {
+
+	private AdhocObjectFactory objectFactory;
+	private JdbcEventLogger logger;
+	private String userName;
+	private String password;
+	private String driver;
+	private String url;
+	private PoolingDataSourceParameters poolParameters;
+
+	public static DataSourceBuilder builder(AdhocObjectFactory objectFactory, JdbcEventLogger logger) {
+		return new DataSourceBuilder(objectFactory, logger);
+	}
+
+	private DataSourceBuilder(AdhocObjectFactory objectFactory, JdbcEventLogger logger) {
+		this.objectFactory = objectFactory;
+		this.logger = logger;
+		this.logger = NoopJdbcEventLogger.getInstance();
+		this.poolParameters = new PoolingDataSourceParameters();
+
+		poolParameters.setMinConnections(1);
+		poolParameters.setMaxConnections(1);
+		poolParameters.setMaxQueueWaitTime(PoolingDataSource.MAX_QUEUE_WAIT_DEFAULT);
+	}
+
+	public DataSourceBuilder userName(String userName) {
+		this.userName = userName;
+		return this;
+	}
+
+	public DataSourceBuilder password(String password) {
+		this.password = password;
+		return this;
+	}
+
+	public DataSourceBuilder driver(String driver) {
+		this.driver = driver;
+		return this;
+	}
+
+	public DataSourceBuilder url(String url) {
+		this.url = url;
+		return this;
+	}
+
+	public DataSourceBuilder minConnections(int minConnections) {
+		poolParameters.setMinConnections(minConnections);
+		return this;
+	}
+
+	public DataSourceBuilder maxConnections(int maxConnections) {
+		poolParameters.setMaxConnections(maxConnections);
+		return this;
+	}
+
+	public DataSourceBuilder maxQueueWaitTime(long maxQueueWaitTime) {
+		poolParameters.setMaxQueueWaitTime(maxQueueWaitTime);
+		return this;
+	}
+
+	public DataSource build() {
+
+		// sanity checks...
+		if (poolParameters.getMaxConnections() < 0) {
+			throw new CayenneRuntimeException("Maximum number of connections can not be negative ("
+					+ poolParameters.getMaxConnections() + ").");
+		}
+
+		if (poolParameters.getMinConnections() < 0) {
+			throw new CayenneRuntimeException("Minimum number of connections can not be negative ("
+					+ poolParameters.getMinConnections() + ").");
+		}
+
+		if (poolParameters.getMinConnections() > poolParameters.getMaxConnections()) {
+			throw new CayenneRuntimeException("Minimum number of connections can not be bigger then maximum.");
+		}
+
+		DataSource nonPooling = buildNonPoolingDataSource();
+		return buildPoolingDataSource(new PooledConnectionFactory(nonPooling));
+	}
+
+	private DataSource buildNonPoolingDataSource() {
+		Driver driver = objectFactory.newInstance(Driver.class, this.driver);
+		DriverDataSource dataSource = new DriverDataSource(driver, url, userName, password);
+		dataSource.setLogger(logger);
+		return dataSource;
+	}
+
+	private DataSource buildPoolingDataSource(PooledConnectionFactory connectionFactory) {
+		PoolingDataSource poolDS;
+		try {
+			poolDS = new PoolingDataSource(connectionFactory, poolParameters);
+		} catch (Exception e) {
+			logger.logConnectFailure(e);
+			throw new CayenneRuntimeException("Error creating DataSource", e);
+		}
+
+		return new ManagedPoolingDataSource(poolDS);
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
index 44c95c1..2bb2664 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/DataSourceInfo.java
@@ -39,6 +39,11 @@ public class DataSourceInfo implements Cloneable, Serializable, XMLSerializable
 
 	private static Log logger = LogFactory.getLog(DataSourceInfo.class);
 
+	public static final String PASSWORD_LOCATION_CLASSPATH = "classpath";
+	public static final String PASSWORD_LOCATION_EXECUTABLE = "executable";
+	public static final String PASSWORD_LOCATION_MODEL = "model";
+	public static final String PASSWORD_LOCATION_URL = "url";
+
 	protected String userName;
 	protected String password;
 	protected String jdbcDriver;
@@ -46,14 +51,6 @@ public class DataSourceInfo implements Cloneable, Serializable, XMLSerializable
 	protected String adapterClassName;
 	protected int minConnections = 1;
 	protected int maxConnections = 1;
-
-	// Constants for passwordLocation
-	public static final String PASSWORD_LOCATION_CLASSPATH = "classpath";
-	public static final String PASSWORD_LOCATION_EXECUTABLE = "executable";
-	public static final String PASSWORD_LOCATION_MODEL = "model";
-	public static final String PASSWORD_LOCATION_URL = "url";
-
-	// Extended parameters
 	protected String passwordEncoderClass = PlainTextPasswordEncoder.class.getName();
 	protected String passwordEncoderKey = "";
 	protected String passwordLocation = PASSWORD_LOCATION_MODEL;
@@ -64,43 +61,72 @@ public class DataSourceInfo implements Cloneable, Serializable, XMLSerializable
 
 	@Override
 	public boolean equals(Object obj) {
-		if (obj == this)
+		
+		if (obj == this) {
 			return true;
+		}
 
-		if (obj == null)
+		if (obj == null) {
 			return false;
+		}
 
-		if (obj.getClass() != this.getClass())
+		if (obj.getClass() != this.getClass()) {
 			return false;
+		}
 
 		DataSourceInfo dsi = (DataSourceInfo) obj;
 
-		if (!Util.nullSafeEquals(this.userName, dsi.userName))
+		if (!Util.nullSafeEquals(this.userName, dsi.userName)) {
 			return false;
-		if (!Util.nullSafeEquals(this.password, dsi.password))
+		}
+
+		if (!Util.nullSafeEquals(this.password, dsi.password)) {
 			return false;
-		if (!Util.nullSafeEquals(this.jdbcDriver, dsi.jdbcDriver))
+		}
+
+		if (!Util.nullSafeEquals(this.jdbcDriver, dsi.jdbcDriver)) {
 			return false;
-		if (!Util.nullSafeEquals(this.dataSourceUrl, dsi.dataSourceUrl))
+		}
+
+		if (!Util.nullSafeEquals(this.dataSourceUrl, dsi.dataSourceUrl)) {
 			return false;
-		if (!Util.nullSafeEquals(this.adapterClassName, dsi.adapterClassName))
+		}
+
+		if (!Util.nullSafeEquals(this.adapterClassName, dsi.adapterClassName)) {
 			return false;
-		if (this.minConnections != dsi.minConnections)
+		}
+
+		if (this.minConnections != dsi.minConnections) {
 			return false;
-		if (this.maxConnections != dsi.maxConnections)
+		}
+
+		if (this.maxConnections != dsi.maxConnections) {
 			return false;
-		if (!Util.nullSafeEquals(this.passwordEncoderClass, dsi.passwordEncoderClass))
+		}
+
+		if (!Util.nullSafeEquals(this.passwordEncoderClass, dsi.passwordEncoderClass)) {
 			return false;
-		if (!Util.nullSafeEquals(this.passwordEncoderKey, dsi.passwordEncoderKey))
+		}
+
+		if (!Util.nullSafeEquals(this.passwordEncoderKey, dsi.passwordEncoderKey)) {
 			return false;
-		if (!Util.nullSafeEquals(this.passwordSourceFilename, dsi.passwordSourceFilename))
+		}
+
+		if (!Util.nullSafeEquals(this.passwordSourceFilename, dsi.passwordSourceFilename)) {
 			return false;
-		if (!Util.nullSafeEquals(this.passwordSourceModel, dsi.passwordSourceModel))
+		}
+
+		if (!Util.nullSafeEquals(this.passwordSourceModel, dsi.passwordSourceModel)) {
 			return false;
-		if (!Util.nullSafeEquals(this.passwordSourceUrl, dsi.passwordSourceUrl))
+		}
+
+		if (!Util.nullSafeEquals(this.passwordSourceUrl, dsi.passwordSourceUrl)) {
 			return false;
-		if (!Util.nullSafeEquals(this.passwordLocation, dsi.passwordLocation))
+		}
+
+		if (!Util.nullSafeEquals(this.passwordLocation, dsi.passwordLocation)) {
 			return false;
+		}
 
 		return true;
 	}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/ManagedPoolingDataSource.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/ManagedPoolingDataSource.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/ManagedPoolingDataSource.java
new file mode 100644
index 0000000..357c3e0
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/ManagedPoolingDataSource.java
@@ -0,0 +1,155 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.conn;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.logging.Logger;
+
+import javax.sql.DataSource;
+
+import org.apache.cayenne.di.ScopeEventListener;
+
+/**
+ * A wrapper for {@link PoolingDataSourceManager} that manages the underlying
+ * connection pool size, shrinking it if needed.
+ * 
+ * @since 4.0
+ */
+public class ManagedPoolingDataSource implements DataSource, ScopeEventListener {
+
+	private PoolingDataSourceManager dataSourceManager;
+	private PoolingDataSource dataSource;
+
+	public ManagedPoolingDataSource(PoolingDataSource dataSource) {
+
+		this.dataSource = dataSource;
+		this.dataSourceManager = new PoolingDataSourceManager();
+
+		dataSourceManager.start();
+	}
+
+	@Override
+	public void beforeScopeEnd() {
+		dataSourceManager.shouldStop();
+	}
+
+	@Override
+	public Connection getConnection() throws SQLException {
+		return dataSource.getConnection();
+	}
+
+	@Override
+	public Connection getConnection(String username, String password) throws SQLException {
+		return dataSource.getConnection(username, password);
+	}
+
+	@Override
+	public PrintWriter getLogWriter() throws SQLException {
+		return dataSource.getLogWriter();
+	}
+
+	@Override
+	public int getLoginTimeout() throws SQLException {
+		return dataSource.getLoginTimeout();
+	}
+
+	@Override
+	public boolean isWrapperFor(Class<?> iface) throws SQLException {
+		return (ManagedPoolingDataSource.class.equals(iface)) ? true : dataSource.isWrapperFor(iface);
+	}
+
+	@Override
+	public void setLogWriter(PrintWriter arg0) throws SQLException {
+		dataSource.setLogWriter(arg0);
+	}
+
+	@Override
+	public void setLoginTimeout(int arg0) throws SQLException {
+		dataSource.setLoginTimeout(arg0);
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public <T> T unwrap(Class<T> iface) throws SQLException {
+		return ManagedPoolingDataSource.class.equals(iface) ? (T) this : dataSource.unwrap(iface);
+	}
+
+	// JDBC 4.1 compatibility under Java 1.6 and newer
+	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+		throw new UnsupportedOperationException();
+	}
+
+	boolean shouldShrinkPool() {
+		int unused = dataSource.getCurrentlyUnused();
+		int used = dataSource.getCurrentlyInUse();
+		int total = unused + used;
+		int median = dataSource.getMinConnections() + 1
+				+ (dataSource.getMaxConnections() - dataSource.getMinConnections()) / 2;
+
+		return unused > 0 && total > median;
+	}
+
+	class PoolingDataSourceManager extends Thread {
+
+		private volatile boolean shouldStop;
+
+		PoolingDataSourceManager() {
+			setName("PoolManagerCleanup-" + dataSource.hashCode());
+			setDaemon(true);
+			this.shouldStop = false;
+		}
+
+		public void shouldStop() {
+			shouldStop = true;
+			interrupt();
+		}
+
+		@Override
+		public void run() {
+			while (true) {
+
+				try {
+					// don't do it too often
+					Thread.sleep(600000);
+				} catch (InterruptedException iex) {
+					// ignore...
+				}
+
+				synchronized (dataSource) {
+
+					// simple pool management - close one connection if the
+					// count is
+					// above median and there are any idle connections.
+
+					if (shouldStop) {
+						break;
+					}
+
+					if (shouldShrinkPool()) {
+						dataSource.shrinkPool(1);
+					}
+				}
+			}
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolDataSource.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolDataSource.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolDataSource.java
deleted file mode 100644
index ad0b33e..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolDataSource.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.conn;
-
-import java.io.PrintWriter;
-import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.util.logging.Logger;
-
-import javax.sql.ConnectionPoolDataSource;
-import javax.sql.DataSource;
-import javax.sql.PooledConnection;
-
-/**
- * PoolDataSource allows to generate pooled connections.
- *
- * <p>
- * It is implemented as a wrapper around a non-pooled data source object.
- * Delegates all method calls except for "getPooledConnection" to the underlying
- * DataSource.
- * 
- */
-public class PoolDataSource implements ConnectionPoolDataSource {
-
-	private DataSource nonPooledDatasource;
-
-	public PoolDataSource(DataSource nonPooledDatasource) {
-		this.nonPooledDatasource = nonPooledDatasource;
-	}
-
-	public PoolDataSource(String jdbcDriver, String connectionUrl) throws SQLException {
-		nonPooledDatasource = new DriverDataSource(jdbcDriver, connectionUrl);
-	}
-
-	@Override
-	public int getLoginTimeout() throws SQLException {
-		return nonPooledDatasource.getLoginTimeout();
-	}
-
-	@Override
-	public void setLoginTimeout(int seconds) throws SQLException {
-		nonPooledDatasource.setLoginTimeout(seconds);
-	}
-
-	@Override
-	public PrintWriter getLogWriter() throws SQLException {
-		return nonPooledDatasource.getLogWriter();
-	}
-
-	@Override
-	public void setLogWriter(PrintWriter out) throws SQLException {
-		nonPooledDatasource.setLogWriter(out);
-	}
-
-	@Override
-	public PooledConnection getPooledConnection() throws SQLException {
-		return new PooledConnectionImpl(nonPooledDatasource, null, null);
-	}
-
-	@Override
-	public PooledConnection getPooledConnection(String user, String password) throws SQLException {
-		return new PooledConnectionImpl(nonPooledDatasource, user, password);
-	}
-
-	/**
-	 * @since 3.1
-	 *
-	 *        JDBC 4.1 compatibility under Java 1.7
-	 */
-	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
-		throw new UnsupportedOperationException();
-	}
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolManager.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolManager.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolManager.java
deleted file mode 100644
index 1a71024..0000000
--- a/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolManager.java
+++ /dev/null
@@ -1,649 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.conn;
-
-import java.io.PrintWriter;
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.logging.Logger;
-
-import javax.sql.ConnectionEvent;
-import javax.sql.ConnectionEventListener;
-import javax.sql.ConnectionPoolDataSource;
-import javax.sql.DataSource;
-import javax.sql.PooledConnection;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.di.ScopeEventListener;
-import org.apache.cayenne.log.JdbcEventLogger;
-
-/**
- * PoolManager is a Cayenne implementation of a pooling DataSource.
- */
-public class PoolManager implements ScopeEventListener, DataSource,
-        ConnectionEventListener {
-
-    /**
-     * Defines a maximum time in milliseconds that a connection request could wait in the
-     * connection queue. After this period expires, an exception will be thrown in the
-     * calling method. 
-     */
-    public static final int MAX_QUEUE_WAIT_DEFAULT = 20000;
-
-    /**
-     * An exception indicating that a connection request waiting in the queue
-     * timed out and was unable to obtain a connection.
-     */
-    public static class ConnectionUnavailableException extends SQLException {
-        private static final long serialVersionUID = 1063973806941023165L;
-
-        public ConnectionUnavailableException(String message) {
-    		super(message);
-    	}
-    }
-    
-    protected ConnectionPoolDataSource poolDataSource;
-    protected int minConnections;
-    protected int maxConnections;
-    protected String dataSourceUrl;
-    protected String jdbcDriver;
-    protected String password;
-    protected String userName;
-
-    protected List<PooledConnection> unusedPool;
-    protected List<PooledConnection> usedPool;
-
-    private PoolMaintenanceThread poolMaintenanceThread;
-
-    private boolean shuttingDown;
-    private long maxQueueWaitTime;
-    
-    /**
-     * Creates new PoolManager using org.apache.cayenne.conn.PoolDataSource for an
-     * underlying ConnectionPoolDataSource.
-     * 
-     * @deprecated since 4.0 This constructor causes implicit class loading that should avoided.
-     */
-    @Deprecated
-    public PoolManager(String jdbcDriver, String dataSourceUrl, int minCons, int maxCons,
-            String userName, String password) throws SQLException {
-
-        this(jdbcDriver, dataSourceUrl, minCons, maxCons, userName, password, null, MAX_QUEUE_WAIT_DEFAULT);
-    }
-
-    /**
-     * @deprecated since 4.0 This constructor causes implicit class loading that should avoided.
-     */
-    @Deprecated
-    public PoolManager(String jdbcDriver, String dataSourceUrl, int minCons, int maxCons,
-            String userName, String password, JdbcEventLogger logger, long maxQueueWaitTime) throws SQLException {
-
-        if (logger != null) {
-            DataSourceInfo info = new DataSourceInfo();
-            info.setJdbcDriver(jdbcDriver);
-            info.setDataSourceUrl(dataSourceUrl);
-            info.setMinConnections(minCons);
-            info.setMaxConnections(maxCons);
-            info.setUserName(userName);
-            info.setPassword(password);
-            logger.logPoolCreated(info);
-        }
-
-        this.jdbcDriver = jdbcDriver;
-        this.dataSourceUrl = dataSourceUrl;
-        DriverDataSource driverDS = new DriverDataSource(jdbcDriver, dataSourceUrl);
-        driverDS.setLogger(logger);
-        PoolDataSource poolDS = new PoolDataSource(driverDS);
-        init(poolDS, minCons, maxCons, userName, password, maxQueueWaitTime);
-    }
-    
-    /**
-     * Creates new PoolManager with the specified policy for connection pooling and a
-     * ConnectionPoolDataSource object.
-     * 
-     * @param poolDataSource data source for pooled connections
-     * @param minCons Non-negative integer that specifies a minimum number of open
-     *            connections to keep in the pool at all times
-     * @param maxCons Non-negative integer that specifies maximum number of simultaneuosly
-     *            open connections
-     * @throws SQLException if pool manager can not be created.
-     * @deprecated since 4.0 use {@link #PoolManager(ConnectionPoolDataSource, int, int, String, String, long)}
-     */
-    public PoolManager(ConnectionPoolDataSource poolDataSource, int minCons, int maxCons,
-            String userName, String password) throws SQLException {
-        this(poolDataSource, minCons, maxCons, userName, password, PoolManager.MAX_QUEUE_WAIT_DEFAULT);
-    }
-
-    /**
-     * Creates new PoolManager with the specified policy for connection pooling and a
-     * ConnectionPoolDataSource object.
-     * 
-     * @param poolDataSource data source for pooled connections
-     * @param minCons Non-negative integer that specifies a minimum number of open
-     *            connections to keep in the pool at all times
-     * @param maxCons Non-negative integer that specifies maximum number of simultaneuosly
-     *            open connections
-     * @throws SQLException if pool manager can not be created.
-     * @since 4.0
-     */
-    public PoolManager(ConnectionPoolDataSource poolDataSource, int minCons, int maxCons,
-            String userName, String password, long maxQueueWaitTime) throws SQLException {
-        init(poolDataSource, minCons, maxCons, userName, password, maxQueueWaitTime);
-    }
-
-    /** Initializes pool. Normally called from constructor. */
-    protected void init(
-            ConnectionPoolDataSource poolDataSource,
-            int minCons,
-            int maxCons,
-            String userName,
-            String password,
-            long maxQueueWaitTime) throws SQLException {
-
-        // do sanity checks...
-        if (maxConnections < 0) {
-            throw new SQLException("Maximum number of connections can not be negative ("
-                    + maxCons
-                    + ").");
-        }
-
-        if (minConnections < 0) {
-            throw new SQLException("Minimum number of connections can not be negative ("
-                    + minCons
-                    + ").");
-        }
-
-        if (minConnections > maxConnections) {
-            throw new SQLException(
-                    "Minimum number of connections can not be bigger then maximum.");
-        }
-
-        // init properties
-        this.userName = userName;
-        this.password = password;
-        this.minConnections = minCons;
-        this.maxConnections = maxCons;
-        this.poolDataSource = poolDataSource;
-        this.maxQueueWaitTime = maxQueueWaitTime;
-        
-        // init pool... use linked lists to use the queue in the FIFO manner
-        usedPool = new LinkedList<PooledConnection>();
-        unusedPool = new LinkedList<PooledConnection>();
-        growPool(minConnections, userName, password);
-
-        startMaintenanceThread();
-    }
-
-    protected synchronized void startMaintenanceThread() {
-        disposeOfMaintenanceThread();
-        this.poolMaintenanceThread = new PoolMaintenanceThread(this);
-        this.poolMaintenanceThread.start();
-    }
-
-    /**
-     * Creates and returns new PooledConnection object, adding itself as a listener for
-     * connection events.
-     */
-    protected PooledConnection newPooledConnection(String userName, String password)
-            throws SQLException {
-        PooledConnection connection = (userName != null) ? poolDataSource
-                .getPooledConnection(userName, password) : poolDataSource
-                .getPooledConnection();
-        connection.addConnectionEventListener(this);
-        return connection;
-    }
-
-    /**
-     * Closes all existing connections, drains the pool and stops the maintenance thread.
-     * 
-     * @since 3.1
-     */
-    public synchronized void shutdown() throws SQLException {
-
-        // disposing maintenance thread first to avoid any changes to pools
-        // during shutdown
-        disposeOfMaintenanceThread();
-
-        // using boolean variable instead of locking PoolManager instance due to
-        // possible deadlock during shutdown when one of connections locks its
-        // event listeners list trying to invoke locked PoolManager's listener methods
-        shuttingDown = true;
-
-        ListIterator<PooledConnection> unusedIterator = unusedPool.listIterator();
-        while (unusedIterator.hasNext()) {
-            PooledConnection con = unusedIterator.next();
-            // close connection
-            con.close();
-            // remove connection from the list
-            unusedIterator.remove();
-        }
-
-        // clean used connections
-        ListIterator<PooledConnection> usedIterator = usedPool.listIterator();
-        while (usedIterator.hasNext()) {
-            PooledConnection con = usedIterator.next();
-            // stop listening for connection events
-            con.removeConnectionEventListener(this);
-            // close connection
-            con.close();
-            // remove connection from the list
-            usedIterator.remove();
-        }
-    }
-
-    /**
-     * An implementation of {@link ScopeEventListener} that simply calls
-     * {@link #shutdown()}.
-     * 
-     * @since 3.1
-     */
-    public void beforeScopeEnd() {
-        try {
-            shutdown();
-        }
-        catch (SQLException e) {
-            throw new CayenneRuntimeException("Error while shutting down");
-        }
-    }
-
-    protected void disposeOfMaintenanceThread() {
-        if (poolMaintenanceThread != null) {
-            poolMaintenanceThread.shutdown();
-            poolMaintenanceThread = null;
-        }
-    }
-
-    /**
-     * @return true if at least one more connection can be added to the pool.
-     */
-    protected synchronized boolean canGrowPool() {
-        return getPoolSize() < maxConnections;
-    }
-
-    /**
-     * Increases connection pool by the specified number of connections.
-     * 
-     * @return the actual number of created connections.
-     * @throws SQLException if an error happens when creating a new connection.
-     */
-    protected synchronized int growPool(
-            int addConnections,
-            String userName,
-            String password) throws SQLException {
-
-        int i = 0;
-        int startPoolSize = getPoolSize();
-        for (; i < addConnections && startPoolSize + i < maxConnections; i++) {
-            PooledConnection newConnection = newPooledConnection(userName, password);
-            unusedPool.add(newConnection);
-        }
-
-        return i;
-    }
-
-    protected synchronized void shrinkPool(int closeConnections) {
-        int idleSize = unusedPool.size();
-        for (int i = 0; i < closeConnections && i < idleSize; i++) {
-            PooledConnection con = unusedPool.remove(i);
-
-            try {
-                con.close();
-            }
-            catch (SQLException ex) {
-                // ignore
-            }
-        }
-    }
-
-    /**
-     * Returns maximum number of connections this pool can keep. This parameter when
-     * configured allows to limit the number of simultaneously open connections.
-     */
-    public int getMaxConnections() {
-        return maxConnections;
-    }
-
-    public void setMaxConnections(int maxConnections) {
-        this.maxConnections = maxConnections;
-    }
-
-    /**
-     * Returns the absolute minimum number of connections allowed in this pool at any
-     * moment in time.
-     */
-    public int getMinConnections() {
-        return minConnections;
-    }
-
-    public void setMinConnections(int minConnections) {
-        this.minConnections = minConnections;
-    }
-
-    /**
-     * Returns a database URL used to initialize this pool. Will return null if the pool
-     * was initialized with ConnectionPoolDataSource.
-     */
-    public String getDataSourceUrl() {
-        return dataSourceUrl;
-    }
-
-    /**
-     * Returns a name of a JDBC driver used to initialize this pool. Will return null if
-     * the pool was initialized with ConnectionPoolDataSource.
-     */
-    public String getJdbcDriver() {
-        return jdbcDriver;
-    }
-
-    /** Returns a data source password used to initialize this pool. */
-    public String getPassword() {
-        return password;
-    }
-
-    /** Returns a data source user name used to initialize this pool. */
-    public String getUserName() {
-        return userName;
-    }
-
-    /**
-     * Returns current number of connections.
-     */
-    public synchronized int getPoolSize() {
-        return usedPool.size() + unusedPool.size();
-    }
-
-    /**
-     * Returns the number of connections obtained via this DataSource that are currently
-     * in use by the DataSource clients.
-     */
-    public synchronized int getCurrentlyInUse() {
-        return usedPool.size();
-    }
-
-    /**
-     * Returns the number of connections maintained in the pool that are currently not
-     * used by any clients and are available immediately via <code>getConnection</code>
-     * method.
-     */
-    public synchronized int getCurrentlyUnused() {
-        return unusedPool.size();
-    }
-
-    /**
-     * Returns connection from the pool using internal values of user name and password.
-     * Equivalent to calling:
-     * <p>
-     * <code>ds.getConnection(ds.getUserName(), ds.getPassword())</code>
-     * </p>
-     */
-    public Connection getConnection() throws SQLException {
-        return getConnection(userName, password);
-    }
-
-    /** Returns connection from the pool. */
-    public synchronized Connection getConnection(String userName, String password)
-            throws SQLException {
-
-        if (shuttingDown) {
-            throw new SQLException("Pool manager is shutting down.");
-        }
-
-        PooledConnection pooledConnection = uncheckPooledConnection(userName, password);
-
-        try {
-            return uncheckConnection(pooledConnection);
-        }
-        catch (SQLException ex) {
-
-            try {
-                pooledConnection.close();
-            }
-            catch (SQLException ignored) {
-            }
-
-            // do one reconnect attempt...
-            pooledConnection = uncheckPooledConnection(userName, password);
-            try {
-                return uncheckConnection(pooledConnection);
-            }
-            catch (SQLException reconnectEx) {
-                try {
-                    pooledConnection.close();
-                }
-                catch (SQLException ignored) {
-                }
-
-                throw reconnectEx;
-            }
-        }
-    }
-
-    private Connection uncheckConnection(PooledConnection pooledConnection)
-            throws SQLException {
-        Connection c = pooledConnection.getConnection();
-
-        // only do that on successfully unchecked connection...
-        usedPool.add(pooledConnection);
-        return c;
-    }
-
-    private PooledConnection uncheckPooledConnection(String userName, String password)
-            throws SQLException {
-        // wait for returned connections or the maintenance thread
-        // to bump the pool size...
-
-        if (unusedPool.size() == 0) {
-
-            // first try to open a new connection
-            if (canGrowPool()) {
-                return newPooledConnection(userName, password);
-            }
-
-            // can't open no more... will have to wait for others to return a connection
-
-            // note that if we were woken up
-            // before the full wait period expired, and no connections are
-            // available yet, go back to sleep. Otherwise we don't give a maintenance
-            // thread a chance to increase pool size
-            long waitTill = System.currentTimeMillis() + maxQueueWaitTime;
-        	
-            do {
-                try {
-                    wait(maxQueueWaitTime);
-                }
-                catch (InterruptedException iex) {
-                    // ignoring
-                }
-
-            } while (unusedPool.size() == 0 && (maxQueueWaitTime == 0 || waitTill > System.currentTimeMillis()));
-
-            if (unusedPool.size() == 0) {
-                throw new ConnectionUnavailableException(
-                        "Can't obtain connection. Request timed out. Total used connections: "
-                                + usedPool.size());
-            }
-        }
-
-        // get first connection... lets cycle them in FIFO manner
-        return unusedPool.remove(0);
-    }
-
-    public int getLoginTimeout() throws java.sql.SQLException {
-        return poolDataSource.getLoginTimeout();
-    }
-
-    public void setLoginTimeout(int seconds) throws java.sql.SQLException {
-        poolDataSource.setLoginTimeout(seconds);
-    }
-
-    public PrintWriter getLogWriter() throws java.sql.SQLException {
-        return poolDataSource.getLogWriter();
-    }
-
-    public void setLogWriter(PrintWriter out) throws java.sql.SQLException {
-        poolDataSource.setLogWriter(out);
-    }
-
-    /**
-     * Returns closed connection to the pool.
-     */
-    public synchronized void connectionClosed(ConnectionEvent event) {
-
-        if (shuttingDown) {
-            return;
-        }
-
-        // return connection to the pool
-        PooledConnection closedConn = (PooledConnection) event.getSource();
-
-        // remove this connection from the list of connections
-        // managed by this pool...
-        int usedInd = usedPool.indexOf(closedConn);
-        if (usedInd >= 0) {
-            usedPool.remove(usedInd);
-            unusedPool.add(closedConn);
-
-            // notify threads waiting for connections
-            notifyAll();
-        }
-        // else ....
-        // other possibility is that this is a bad connection, so just ignore its closing
-        // event,
-        // since it was unregistered in "connectionErrorOccurred"
-    }
-
-    /**
-     * Removes connection with an error from the pool. This method is called by
-     * PoolManager connections on connection errors to notify PoolManager that connection
-     * is in invalid state.
-     */
-    public synchronized void connectionErrorOccurred(ConnectionEvent event) {
-
-        if (shuttingDown) {
-            return;
-        }
-
-        // later on we should analyze the error to see if this
-        // is fatal... right now just kill this PooledConnection
-
-        PooledConnection errorSrc = (PooledConnection) event.getSource();
-
-        // remove this connection from the list of connections
-        // managed by this pool...
-
-        int usedInd = usedPool.indexOf(errorSrc);
-        if (usedInd >= 0) {
-            usedPool.remove(usedInd);
-        }
-        else {
-            int unusedInd = unusedPool.indexOf(errorSrc);
-            if (unusedInd >= 0)
-                unusedPool.remove(unusedInd);
-        }
-
-        // do not close connection,
-        // let the code that catches the exception handle it
-        // ....
-    }
-
-    static class PoolMaintenanceThread extends Thread {
-
-        private boolean shouldDie;
-        private PoolManager pool;
-
-        PoolMaintenanceThread(PoolManager pool) {
-            super.setName("PoolManagerCleanup-" + pool.hashCode());
-            super.setDaemon(true);
-            this.pool = pool;
-        }
-
-        @Override
-        public void run() {
-            // periodically wakes up to check if the pool should grow or shrink
-            while (true) {
-
-                try {
-                    // don't do it too often
-                    sleep(600000);
-                }
-                catch (InterruptedException iex) {
-                    // ignore...
-                }
-
-                synchronized (pool) {
-                    // TODO: implement a smarter algorithm for pool management...
-                    // right now it will simply close one connection if the count is
-                    // above median and there are any idle connections.
-
-                    if (shouldDie) {
-                        break;
-                    }
-
-                    int unused = pool.getCurrentlyUnused();
-                    int used = pool.getCurrentlyInUse();
-                    int total = unused + used;
-                    int median = pool.minConnections
-                            + 1
-                            + (pool.maxConnections - pool.minConnections)
-                            / 2;
-
-                    if (unused > 0 && total > median) {
-                        pool.shrinkPool(1);
-                    }
-                }
-            }
-        }
-
-        /**
-         * Stops the maintenance thread.
-         */
-        void shutdown() {
-            shouldDie = true;
-            interrupt();
-        }
-    }
-
-    /**
-     * @since 3.0
-     */
-    // JDBC 4 compatibility under Java 1.5
-    public boolean isWrapperFor(Class<?> iface) throws SQLException {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * @since 3.0
-     */
-    // JDBC 4 compatibility under Java 1.5
-    public <T> T unwrap(Class<T> iface) throws SQLException {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * @since 3.1 JDBC 4.1 compatibility under Java 1.5
-     */
-    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
-        throw new UnsupportedOperationException();
-    }
-}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionFactory.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionFactory.java
new file mode 100644
index 0000000..2ebfeab
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionFactory.java
@@ -0,0 +1,90 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.conn;
+
+import java.io.PrintWriter;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.logging.Logger;
+
+import javax.sql.ConnectionPoolDataSource;
+import javax.sql.DataSource;
+import javax.sql.PooledConnection;
+
+/**
+ * A {@link ConnectionPoolDataSource} implementation.
+ * <p>
+ * It is implemented as a wrapper around a non-pooled data source object.
+ * Delegates all method calls except for "getPooledConnection" to the underlying
+ * DataSource.
+ * 
+ * @since 4.0
+ */
+public class PooledConnectionFactory implements ConnectionPoolDataSource {
+
+	private DataSource nonPooledDatasource;
+
+	public PooledConnectionFactory(DataSource nonPooledDatasource) {
+		this.nonPooledDatasource = nonPooledDatasource;
+	}
+
+	public PooledConnectionFactory(String jdbcDriver, String connectionUrl) throws SQLException {
+		nonPooledDatasource = new DriverDataSource(jdbcDriver, connectionUrl);
+	}
+
+	@Override
+	public int getLoginTimeout() throws SQLException {
+		return nonPooledDatasource.getLoginTimeout();
+	}
+
+	@Override
+	public void setLoginTimeout(int seconds) throws SQLException {
+		nonPooledDatasource.setLoginTimeout(seconds);
+	}
+
+	@Override
+	public PrintWriter getLogWriter() throws SQLException {
+		return nonPooledDatasource.getLogWriter();
+	}
+
+	@Override
+	public void setLogWriter(PrintWriter out) throws SQLException {
+		nonPooledDatasource.setLogWriter(out);
+	}
+
+	@Override
+	public PooledConnection getPooledConnection() throws SQLException {
+		return new PooledConnectionImpl(nonPooledDatasource, null, null);
+	}
+
+	@Override
+	public PooledConnection getPooledConnection(String user, String password) throws SQLException {
+		return new PooledConnectionImpl(nonPooledDatasource, user, password);
+	}
+
+	/**
+	 * @since 3.1
+	 *
+	 *        JDBC 4.1 compatibility under Java 1.7
+	 */
+	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+		throw new UnsupportedOperationException();
+	}
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionImpl.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionImpl.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionImpl.java
index f63dcb8..eb327ff 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionImpl.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/PooledConnectionImpl.java
@@ -32,170 +32,164 @@ import javax.sql.PooledConnection;
 import javax.sql.StatementEventListener;
 
 /**
- * PooledConnectionImpl is an implementation of a pooling wrapper for the database
- * connection as per JDBC3 spec. Most of the modern JDBC drivers should have its own
- * implementation that may be used instead of this class.
- * 
+ * PooledConnectionImpl is an implementation of a pooling wrapper for the
+ * database connection as per JDBC3 spec. Most of the modern JDBC drivers should
+ * have its own implementation that may be used instead of this class.
  */
 public class PooledConnectionImpl implements PooledConnection {
 
-    private Connection connectionObj;
-    private List<ConnectionEventListener> connectionEventListeners;
-    private boolean hadErrors;
-    private DataSource connectionSource;
-    private String userName;
-    private String password;
-
-    protected PooledConnectionImpl() {
-        // TODO: maybe remove synchronization and use
-        // FastArrayList from commons-collections? After
-        // all the only listener is usually pool manager.
-        this.connectionEventListeners = Collections
-                .synchronizedList(new ArrayList<ConnectionEventListener>(10));
-    }
-
-    /** Creates new PooledConnection */
-    public PooledConnectionImpl(DataSource connectionSource, String userName,
-            String password) {
-
-        this();
-
-        this.connectionSource = connectionSource;
-        this.userName = userName;
-        this.password = password;
-
-    }
-
-    public void reconnect() throws SQLException {
-        if (connectionObj != null) {
-            try {
-                connectionObj.close();
-            }
-            catch (SQLException ex) {
-                // ignore exception, since connection is expected
-                // to be in a bad state
-            }
-            finally {
-                connectionObj = null;
-            }
-        }
-
-        connectionObj = (userName != null) ? connectionSource.getConnection(
-                userName,
-                password) : connectionSource.getConnection();
-    }
-
-    public void addConnectionEventListener(ConnectionEventListener listener) {
-        synchronized (connectionEventListeners) {
-            if (!connectionEventListeners.contains(listener))
-                connectionEventListeners.add(listener);
-        }
-    }
-
-    public void removeConnectionEventListener(ConnectionEventListener listener) {
-        synchronized (connectionEventListeners) {
-            connectionEventListeners.remove(listener);
-        }
-    }
-
-    public void close() throws SQLException {
-
-        synchronized (connectionEventListeners) {
-            // remove all listeners
-            connectionEventListeners.clear();
-        }
-
-        if (connectionObj != null) {
-            try {
-                connectionObj.close();
-            }
-            finally {
-                connectionObj = null;
-            }
-        }
-    }
-
-    public Connection getConnection() throws SQLException {
-        if (connectionObj == null) {
-            reconnect();
-        }
-
-        // set autocommit to false to return connection
-        // always in consistent state
-        if (!connectionObj.getAutoCommit()) {
-
-            try {
-                connectionObj.setAutoCommit(true);
-            }
-            catch (SQLException sqlEx) {
-                // try applying Sybase patch
-                ConnectionWrapper.sybaseAutoCommitPatch(connectionObj, sqlEx, true);
-            }
-        }
-
-        connectionObj.clearWarnings();
-        return new ConnectionWrapper(connectionObj, this);
-    }
-
-    protected void returnConnectionToThePool() throws SQLException {
-        // do not return to pool bad connections
-        if (hadErrors)
-            close();
-        else
-            // notify the listeners that connection is no longer used by application...
-            this.connectionClosedNotification();
-    }
-
-    /**
-     * This method creates and sents an event to listeners when an error occurs in the
-     * underlying connection. Listeners can have special logic to analyze the error and do
-     * things like closing this PooledConnection (if the error is fatal), etc...
-     */
-    public void connectionErrorNotification(SQLException exception) {
-        // hint for later to avoid returning bad connections to the pool
-        hadErrors = true;
-
-        synchronized (connectionEventListeners) {
-            if (connectionEventListeners.size() == 0)
-                return;
-
-            ConnectionEvent closedEvent = new ConnectionEvent(this, exception);
-            for (final ConnectionEventListener nextListener : connectionEventListeners) {
-                nextListener.connectionErrorOccurred(closedEvent);
-            }
-        }
-    }
-
-    /**
-     * Creates and sends an event to listeners when a user closes java.sql.Connection
-     * object belonging to this PooledConnection.
-     */
-    protected void connectionClosedNotification() {
-        synchronized (connectionEventListeners) {
-            if (connectionEventListeners.size() == 0)
-                return;
-
-            ConnectionEvent closedEvent = new ConnectionEvent(this);
-
-            for (final ConnectionEventListener nextListener : connectionEventListeners) {
-                nextListener.connectionClosed(closedEvent);
-            }
-        }
-    }
-
-    /**
-     * @since 3.0
-     */
-    // JDBC 4 compatibility under Java 1.5
-    public void addStatementEventListener(StatementEventListener listener) {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * @since 3.0
-     */
-    // JDBC 4 compatibility under Java 1.5
-    public void removeStatementEventListener(StatementEventListener listener) {
-        throw new UnsupportedOperationException();
-    }
+	private Connection connectionObj;
+	private List<ConnectionEventListener> connectionEventListeners;
+	private boolean hadErrors;
+	private DataSource dataSource;
+	private String userName;
+	private String password;
+
+	public PooledConnectionImpl(DataSource dataSource, String userName, String password) {
+
+		// TODO: maybe remove synchronization and use
+		// FastArrayList from commons-collections? After
+		// all the only listener is usually pool manager.
+		this.connectionEventListeners = Collections.synchronizedList(new ArrayList<ConnectionEventListener>(10));
+
+		this.dataSource = dataSource;
+		this.userName = userName;
+		this.password = password;
+	}
+
+	public void reconnect() throws SQLException {
+		if (connectionObj != null) {
+			try {
+				connectionObj.close();
+			} catch (SQLException ex) {
+				// ignore exception, since connection is expected
+				// to be in a bad state
+			} finally {
+				connectionObj = null;
+			}
+		}
+
+		connectionObj = (userName != null) ? dataSource.getConnection(userName, password) : dataSource.getConnection();
+	}
+
+	@Override
+	public void addConnectionEventListener(ConnectionEventListener listener) {
+		synchronized (connectionEventListeners) {
+			if (!connectionEventListeners.contains(listener)) {
+				connectionEventListeners.add(listener);
+			}
+		}
+	}
+
+	@Override
+	public void removeConnectionEventListener(ConnectionEventListener listener) {
+		synchronized (connectionEventListeners) {
+			connectionEventListeners.remove(listener);
+		}
+	}
+
+	@Override
+	public void close() throws SQLException {
+
+		synchronized (connectionEventListeners) {
+			// remove all listeners
+			connectionEventListeners.clear();
+		}
+
+		if (connectionObj != null) {
+			try {
+				connectionObj.close();
+			} finally {
+				connectionObj = null;
+			}
+		}
+	}
+
+	@Override
+	public Connection getConnection() throws SQLException {
+		if (connectionObj == null) {
+			reconnect();
+		}
+
+		// set autocommit to false to return connection
+		// always in consistent state
+		if (!connectionObj.getAutoCommit()) {
+
+			try {
+				connectionObj.setAutoCommit(true);
+			} catch (SQLException sqlEx) {
+				// try applying Sybase patch
+				ConnectionWrapper.sybaseAutoCommitPatch(connectionObj, sqlEx, true);
+			}
+		}
+
+		connectionObj.clearWarnings();
+		return new ConnectionWrapper(connectionObj, this);
+	}
+
+	protected void returnConnectionToThePool() throws SQLException {
+		// do not return to pool bad connections
+		if (hadErrors)
+			close();
+		else
+			// notify the listeners that connection is no longer used by
+			// application...
+			this.connectionClosedNotification();
+	}
+
+	/**
+	 * This method creates and sents an event to listeners when an error occurs
+	 * in the underlying connection. Listeners can have special logic to analyze
+	 * the error and do things like closing this PooledConnection (if the error
+	 * is fatal), etc...
+	 */
+	public void connectionErrorNotification(SQLException exception) {
+		// hint for later to avoid returning bad connections to the pool
+		hadErrors = true;
+
+		synchronized (connectionEventListeners) {
+			if (connectionEventListeners.isEmpty()) {
+				return;
+			}
+
+			ConnectionEvent closedEvent = new ConnectionEvent(this, exception);
+			for (ConnectionEventListener nextListener : connectionEventListeners) {
+				nextListener.connectionErrorOccurred(closedEvent);
+			}
+		}
+	}
+
+	/**
+	 * Creates and sends an event to listeners when a user closes
+	 * java.sql.Connection object belonging to this PooledConnection.
+	 */
+	protected void connectionClosedNotification() {
+		synchronized (connectionEventListeners) {
+			if (connectionEventListeners.size() == 0) {
+				return;
+			}
+
+			ConnectionEvent closedEvent = new ConnectionEvent(this);
+
+			for (ConnectionEventListener nextListener : connectionEventListeners) {
+				nextListener.connectionClosed(closedEvent);
+			}
+		}
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	@Override
+	public void addStatementEventListener(StatementEventListener listener) {
+		throw new UnsupportedOperationException("Statement events are unsupported");
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	@Override
+	public void removeStatementEventListener(StatementEventListener listener) {
+		throw new UnsupportedOperationException("Statement events are unsupported");
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSource.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSource.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSource.java
new file mode 100644
index 0000000..cccf905
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSource.java
@@ -0,0 +1,471 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.conn;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Statement;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.logging.Logger;
+
+import javax.sql.ConnectionEvent;
+import javax.sql.ConnectionEventListener;
+import javax.sql.ConnectionPoolDataSource;
+import javax.sql.DataSource;
+import javax.sql.PooledConnection;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.di.ScopeEventListener;
+
+/**
+ * A {@link DataSource} with a pool of connections, that can automatically grow
+ * to the max size as more connections are requested.
+ * 
+ * @since 4.0
+ */
+public class PoolingDataSource implements ScopeEventListener, DataSource, ConnectionEventListener {
+
+	/**
+	 * Defines a maximum time in milliseconds that a connection request could
+	 * wait in the connection queue. After this period expires, an exception
+	 * will be thrown in the calling method.
+	 */
+	public static final int MAX_QUEUE_WAIT_DEFAULT = 20000;
+
+	/**
+	 * An exception indicating that a connection request waiting in the queue
+	 * timed out and was unable to obtain a connection.
+	 */
+	public static class ConnectionUnavailableException extends SQLException {
+		private static final long serialVersionUID = 1063973806941023165L;
+
+		public ConnectionUnavailableException(String message) {
+			super(message);
+		}
+	}
+
+	protected ConnectionPoolDataSource pooledConnectionFactory;
+
+	private int minConnections;
+	private int maxConnections;
+	private long maxQueueWaitTime;
+	private String validationQuery;
+
+	private List<PooledConnection> unusedPool;
+	private List<PooledConnection> usedPool;
+	private boolean shuttingDown;
+
+	/**
+	 * Creates new PoolManager with the specified policy for connection pooling
+	 * and a ConnectionPoolDataSource object.
+	 * 
+	 * @param poolDataSource
+	 *            data source for pooled connections
+	 * @param minCons
+	 *            Non-negative integer that specifies a minimum number of open
+	 *            connections to keep in the pool at all times
+	 * @param maxCons
+	 *            Non-negative integer that specifies maximum number of
+	 *            simultaneously open connections
+	 * @throws SQLException
+	 *             if pool manager can not be created.
+	 * @since 4.0
+	 */
+	public PoolingDataSource(ConnectionPoolDataSource poolDataSource, PoolingDataSourceParameters parameters)
+			throws SQLException {
+
+		this.pooledConnectionFactory = poolDataSource;
+
+		// clone parameters to keep DataSource immutable
+		this.minConnections = parameters.getMinConnections();
+		this.maxConnections = parameters.getMaxConnections();
+		this.maxQueueWaitTime = parameters.getMaxQueueWaitTime();
+		this.validationQuery = parameters.getValidationQuery();
+
+		// init pool... use linked lists to use the queue in the FIFO manner
+		this.usedPool = new LinkedList<PooledConnection>();
+		this.unusedPool = new LinkedList<PooledConnection>();
+		growPool(minConnections);
+	}
+
+	/**
+	 * Creates and returns new PooledConnection object, adding itself as a
+	 * listener for connection events.
+	 */
+	protected PooledConnection newPooledConnection() throws SQLException {
+		PooledConnection connection = pooledConnectionFactory.getPooledConnection();
+		connection.addConnectionEventListener(this);
+		return connection;
+	}
+
+	/**
+	 * Shuts down the pool, closing all open connections. This is an
+	 * implementation of {@link ScopeEventListener}.
+	 * 
+	 * @since 3.1
+	 */
+	@Override
+	public synchronized void beforeScopeEnd() {
+		try {
+
+			// using boolean variable instead of locking PoolManager instance
+			// due to possible deadlock during shutdown when one of connections
+			// locks its event listeners list trying to invoke locked
+			// PoolManager's listener methods
+			shuttingDown = true;
+
+			ListIterator<PooledConnection> unusedIterator = unusedPool.listIterator();
+			while (unusedIterator.hasNext()) {
+				PooledConnection con = unusedIterator.next();
+				// close connection
+				con.close();
+				// remove connection from the list
+				unusedIterator.remove();
+			}
+
+			// clean used connections
+			ListIterator<PooledConnection> usedIterator = usedPool.listIterator();
+			while (usedIterator.hasNext()) {
+				PooledConnection con = usedIterator.next();
+				// stop listening for connection events
+				con.removeConnectionEventListener(this);
+				// close connection
+				con.close();
+				// remove connection from the list
+				usedIterator.remove();
+			}
+		} catch (SQLException e) {
+			throw new CayenneRuntimeException("Error while shutting down");
+		}
+	}
+
+	/**
+	 * @return true if at least one more connection can be added to the pool.
+	 */
+	protected synchronized boolean canGrowPool() {
+		return getPoolSize() < maxConnections;
+	}
+
+	/**
+	 * Increases connection pool by the specified number of connections.
+	 * 
+	 * @return the actual number of created connections.
+	 * @throws SQLException
+	 *             if an error happens when creating a new connection.
+	 */
+	protected synchronized int growPool(int addConnections) throws SQLException {
+
+		int i = 0;
+		int startPoolSize = getPoolSize();
+		for (; i < addConnections && startPoolSize + i < maxConnections; i++) {
+			PooledConnection newConnection = newPooledConnection();
+			unusedPool.add(newConnection);
+		}
+
+		return i;
+	}
+
+	public synchronized void shrinkPool(int closeConnections) {
+		int idleSize = unusedPool.size();
+		for (int i = 0; i < closeConnections && i < idleSize; i++) {
+			PooledConnection con = unusedPool.remove(i);
+
+			try {
+				con.close();
+			} catch (SQLException ex) {
+				// ignore
+			}
+		}
+	}
+
+	public String getValidationQuery() {
+		return validationQuery;
+	}
+
+	/**
+	 * Returns maximum number of connections this pool can keep. This parameter
+	 * when configured allows to limit the number of simultaneously open
+	 * connections.
+	 */
+	public int getMaxConnections() {
+		return maxConnections;
+	}
+
+	/**
+	 * Returns the absolute minimum number of connections allowed in this pool
+	 * at any moment in time.
+	 */
+	public int getMinConnections() {
+		return minConnections;
+	}
+
+	/**
+	 * Returns current number of connections.
+	 */
+	public synchronized int getPoolSize() {
+		return usedPool.size() + unusedPool.size();
+	}
+
+	/**
+	 * Returns the number of connections obtained via this DataSource that are
+	 * currently in use by the DataSource clients.
+	 */
+	public synchronized int getCurrentlyInUse() {
+		return usedPool.size();
+	}
+
+	/**
+	 * Returns the number of connections maintained in the pool that are
+	 * currently not used by any clients and are available immediately via
+	 * <code>getConnection</code> method.
+	 */
+	public synchronized int getCurrentlyUnused() {
+		return unusedPool.size();
+	}
+
+	/**
+	 * Returns connection from the pool using internal values of user name and
+	 * password.
+	 */
+	@Override
+	public synchronized Connection getConnection() throws SQLException {
+		if (shuttingDown) {
+			throw new SQLException("Pool manager is shutting down.");
+		}
+
+		PooledConnection pooledConnection = uncheckPooledConnection();
+
+		try {
+			return uncheckAndValidateConnection(pooledConnection);
+		} catch (SQLException ex) {
+
+			try {
+				pooledConnection.close();
+			} catch (SQLException ignored) {
+			}
+
+			// do one reconnect attempt...
+			pooledConnection = uncheckPooledConnection();
+			try {
+				return uncheckAndValidateConnection(pooledConnection);
+			} catch (SQLException reconnectEx) {
+				try {
+					pooledConnection.close();
+				} catch (SQLException ignored) {
+				}
+
+				throw reconnectEx;
+			}
+		}
+	}
+
+	/**
+	 * Returns connection from the pool.
+	 */
+	@Override
+	public synchronized Connection getConnection(String userName, String password) throws SQLException {
+		throw new UnsupportedOperationException(
+				"Connections for a specific user are not supported by the pooled DataSource");
+	}
+
+	private Connection uncheckConnection(PooledConnection pooledConnection) throws SQLException {
+		Connection c = pooledConnection.getConnection();
+
+		// only do that on successfully unchecked connection...
+		usedPool.add(pooledConnection);
+		return c;
+	}
+
+	private Connection uncheckAndValidateConnection(PooledConnection pooledConnection) throws SQLException {
+		Connection c = uncheckConnection(pooledConnection);
+
+		if (validationQuery != null) {
+
+			Statement statement = c.createStatement();
+			try {
+				ResultSet rs = statement.executeQuery(validationQuery);
+				try {
+
+					if (!rs.next()) {
+						throw new SQLException("Connection validation failed, no result for query: " + validationQuery);
+					}
+
+				} finally {
+					rs.close();
+				}
+			} finally {
+				statement.close();
+			}
+		}
+
+		return c;
+	}
+
+	private PooledConnection uncheckPooledConnection() throws SQLException {
+		// wait for returned connections or the maintenance thread
+		// to bump the pool size...
+
+		if (unusedPool.size() == 0) {
+
+			// first try to open a new connection
+			if (canGrowPool()) {
+				return newPooledConnection();
+			}
+
+			// can't open no more... will have to wait for others to return a
+			// connection
+
+			// note that if we were woken up
+			// before the full wait period expired, and no connections are
+			// available yet, go back to sleep. Otherwise we don't give a
+			// maintenance
+			// thread a chance to increase pool size
+			long waitTill = System.currentTimeMillis() + maxQueueWaitTime;
+
+			do {
+				try {
+					wait(maxQueueWaitTime);
+				} catch (InterruptedException iex) {
+					// ignoring
+				}
+
+			} while (unusedPool.size() == 0 && (maxQueueWaitTime == 0 || waitTill > System.currentTimeMillis()));
+
+			if (unusedPool.size() == 0) {
+				throw new ConnectionUnavailableException(
+						"Can't obtain connection. Request timed out. Total used connections: " + usedPool.size());
+			}
+		}
+
+		// get first connection... lets cycle them in FIFO manner
+		return unusedPool.remove(0);
+	}
+
+	@Override
+	public int getLoginTimeout() throws java.sql.SQLException {
+		return pooledConnectionFactory.getLoginTimeout();
+	}
+
+	@Override
+	public void setLoginTimeout(int seconds) throws java.sql.SQLException {
+		pooledConnectionFactory.setLoginTimeout(seconds);
+	}
+
+	@Override
+	public PrintWriter getLogWriter() throws java.sql.SQLException {
+		return pooledConnectionFactory.getLogWriter();
+	}
+
+	@Override
+	public void setLogWriter(PrintWriter out) throws java.sql.SQLException {
+		pooledConnectionFactory.setLogWriter(out);
+	}
+
+	/**
+	 * Returns closed connection to the pool.
+	 */
+	@Override
+	public synchronized void connectionClosed(ConnectionEvent event) {
+
+		if (shuttingDown) {
+			return;
+		}
+
+		// return connection to the pool
+		PooledConnection closedConn = (PooledConnection) event.getSource();
+
+		// remove this connection from the list of connections
+		// managed by this pool...
+		int usedInd = usedPool.indexOf(closedConn);
+		if (usedInd >= 0) {
+			usedPool.remove(usedInd);
+			unusedPool.add(closedConn);
+
+			// notify threads waiting for connections
+			notifyAll();
+		}
+		// else ....
+		// other possibility is that this is a bad connection, so just ignore
+		// its closing
+		// event,
+		// since it was unregistered in "connectionErrorOccurred"
+	}
+
+	/**
+	 * Removes connection with an error from the pool. This method is called by
+	 * PoolManager connections on connection errors to notify PoolManager that
+	 * connection is in invalid state.
+	 */
+	@Override
+	public synchronized void connectionErrorOccurred(ConnectionEvent event) {
+
+		if (shuttingDown) {
+			return;
+		}
+
+		// later on we should analyze the error to see if this
+		// is fatal... right now just kill this PooledConnection
+
+		PooledConnection errorSrc = (PooledConnection) event.getSource();
+
+		// remove this connection from the list of connections
+		// managed by this pool...
+
+		int usedInd = usedPool.indexOf(errorSrc);
+		if (usedInd >= 0) {
+			usedPool.remove(usedInd);
+		} else {
+			int unusedInd = unusedPool.indexOf(errorSrc);
+			if (unusedInd >= 0) {
+				unusedPool.remove(unusedInd);
+			}
+		}
+
+		// do not close connection,
+		// let the code that catches the exception handle it
+		// ....
+	}
+
+	@Override
+	public boolean isWrapperFor(Class<?> iface) throws SQLException {
+		return PoolingDataSource.class.equals(iface);
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public <T> T unwrap(Class<T> iface) throws SQLException {
+		if (PoolingDataSource.class.equals(iface)) {
+			return (T) this;
+		}
+
+		throw new SQLException("Not a wrapper for " + iface);
+	}
+
+	// JDBC 4.1 compatibility under Java <= 1.6
+	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+		throw new UnsupportedOperationException();
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cayenne/blob/8b54c052/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSourceParameters.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSourceParameters.java b/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSourceParameters.java
new file mode 100644
index 0000000..a03fd39
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/conn/PoolingDataSourceParameters.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.conn;
+
+/**
+ * A connection of pooling parameters used by {@link PoolingDataSource}.
+ * 
+ * @since 4.0
+ */
+public class PoolingDataSourceParameters {
+
+	private String validationQuery;
+	private int minConnections;
+	private int maxConnections;
+	private long maxQueueWaitTime;
+
+	public int getMinConnections() {
+		return minConnections;
+	}
+
+	public void setMinConnections(int minConnections) {
+		this.minConnections = minConnections;
+	}
+
+	public int getMaxConnections() {
+		return maxConnections;
+	}
+
+	public void setMaxConnections(int maxConnections) {
+		this.maxConnections = maxConnections;
+	}
+
+	public long getMaxQueueWaitTime() {
+		return maxQueueWaitTime;
+	}
+
+	public void setMaxQueueWaitTime(long maxQueueWaitTime) {
+		this.maxQueueWaitTime = maxQueueWaitTime;
+	}
+
+	public String getValidationQuery() {
+		return validationQuery;
+	}
+
+	public void setValidationQuery(String validationQuery) {
+		this.validationQuery = validationQuery;
+	}
+}