Use JDBC DataSource to create a Database rather than direct URLs

This way the database can be configured in a web application container
as a JNDI resource that applications can lookup, making it easier to
deploy a web application whose schema is managed by gwtorm.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/com/google/gwtorm/jdbc/Database.java b/src/com/google/gwtorm/jdbc/Database.java
index 954b5bc..7f575f6 100644
--- a/src/com/google/gwtorm/jdbc/Database.java
+++ b/src/com/google/gwtorm/jdbc/Database.java
@@ -27,13 +27,13 @@
 import com.google.gwtorm.schema.sql.SqlDialect;
 
 import java.sql.Connection;
-import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.util.Collections;
 import java.util.Map;
-import java.util.Properties;
 import java.util.WeakHashMap;
 
+import javax.sql.DataSource;
+
 /**
  * Constructor for application {@link Schema} extensions.
  * <p>
@@ -53,56 +53,37 @@
   private static final Map<Class<?>, String> schemaFactoryNames =
       Collections.synchronizedMap(new WeakHashMap<Class<?>, String>());
 
-  private final Properties connectionInfo;
-  private final String url;
+  private final DataSource dataSource;
   private final JavaSchemaModel schemaModel;
   private final AbstractSchemaFactory<T> implFactory;
   private final SqlDialect implDialect;
 
   /**
    * Create a new database interface, generating the interface implementations.
-   * <p>
-   * The JDBC properties information must define at least <code>url</code> and
-   * <code>driver</code>, but may also include driver specific properties such
-   * as <code>username</code> and <code>password</code>.
    * 
-   * @param dbInfo JDBC connection information. The property table is copied.
+   * @param ds JDBC connection information
    * @param schema application extension of the Schema interface to implement.
    * @throws OrmException the schema interface is incorrectly defined, or the
    *         driver class is not available through the current class loader.
    */
-  public Database(final Properties dbInfo, final Class<T> schema)
+  public Database(final DataSource ds, final Class<T> schema)
       throws OrmException {
-    connectionInfo = new Properties();
-    connectionInfo.putAll(dbInfo);
+    dataSource = ds;
 
-    url = (String) connectionInfo.remove("url");
-    if (url == null) {
-      throw new OrmException("Required property 'url' not defined");
-    }
-
-    final String driver = (String) connectionInfo.remove("driver");
-    if (driver != null) {
-      loadDriver(driver);
+    final String url;
+    try {
+      final Connection c = ds.getConnection();
+      try {
+        url = c.getMetaData().getURL();
+      } finally {
+        c.close();
+      }
+    } catch (SQLException e) {
+      throw new OrmException("Unable to determine driver URL", e);
     }
 
     final SqlDialect dialect;
-    String dialectName = (String) connectionInfo.remove("dialect");
-    if (dialectName != null) {
-      if (!dialectName.contains(".")) {
-        final String n = SqlDialect.class.getName();
-        dialectName = n.substring(0, n.lastIndexOf('.') + 1) + dialectName;
-      }
-      try {
-        dialect = (SqlDialect) Class.forName(dialectName).newInstance();
-      } catch (InstantiationException e) {
-        throw new OrmException("Dialect " + dialectName + " not available", e);
-      } catch (IllegalAccessException e) {
-        throw new OrmException("Dialect " + dialectName + " not available", e);
-      } catch (ClassNotFoundException e) {
-        throw new OrmException("Dialect " + dialectName + " not found", e);
-      }
-    } else if (url.startsWith("jdbc:postgresql:")) {
+    if (url.startsWith("jdbc:postgresql:")) {
       dialect = new DialectPostgreSQL();
     } else if (url.startsWith("jdbc:h2:")) {
       dialect = new DialectH2();
@@ -161,7 +142,7 @@
   public T open() throws OrmException {
     final Connection conn;
     try {
-      conn = DriverManager.getConnection(url, connectionInfo);
+      conn = dataSource.getConnection();
     } catch (SQLException e) {
       throw new OrmException("Cannot open database connection", e);
     }
@@ -184,27 +165,4 @@
   private static <T> GeneratedClassLoader newLoader(final Class<T> schema) {
     return new GeneratedClassLoader(schema.getClassLoader());
   }
-
-  private static synchronized void loadDriver(final String driver)
-      throws OrmException {
-    // I've seen some drivers (*cough* Informix *cough*) which won't load
-    // on multiple threads at the same time. Forcing our code to synchronize
-    // around loading the driver ensures we won't ever ask for the same driver
-    // to initialize from different threads. Of course that could still happen
-    // in other parts of the same JVM, but its quite unlikely.
-    //
-    try {
-      Class.forName(driver, true, threadCL());
-    } catch (ClassNotFoundException err) {
-      throw new OrmException("Driver class " + driver + " not available", err);
-    }
-  }
-
-  private static ClassLoader threadCL() {
-    try {
-      return Thread.currentThread().getContextClassLoader();
-    } catch (SecurityException e) {
-      return Database.class.getClassLoader();
-    }
-  }
 }
diff --git a/src/com/google/gwtorm/jdbc/SimpleDataSource.java b/src/com/google/gwtorm/jdbc/SimpleDataSource.java
new file mode 100644
index 0000000..724bf94
--- /dev/null
+++ b/src/com/google/gwtorm/jdbc/SimpleDataSource.java
@@ -0,0 +1,116 @@
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtorm.jdbc;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+/** A simple non-pooling DataSource representation. */
+public class SimpleDataSource implements DataSource {
+  private final Properties connectionInfo;
+  private final String url;
+  private PrintWriter logWriter;
+
+  /**
+   * Create a non-pooling data source.
+   * <p>
+   * The JDBC properties information must define at least <code>url</code> and
+   * <code>driver</code>, but may also include driver specific properties such
+   * as <code>username</code> and <code>password</code>.
+   * 
+   * @param dbInfo JDBC connection information. The property table is copied.
+   * @throws SQLException the driver class is not available through the current
+   *         class loader.
+   */
+  public SimpleDataSource(final Properties dbInfo) throws SQLException {
+    connectionInfo = new Properties();
+    connectionInfo.putAll(dbInfo);
+
+    url = (String) connectionInfo.remove("url");
+    if (url == null) {
+      throw new SQLException("Required property 'url' not defined");
+    }
+
+    final String driver = (String) connectionInfo.remove("driver");
+    if (driver != null) {
+      loadDriver(driver);
+    }
+
+    logWriter = new PrintWriter(System.out);
+  }
+
+  public Connection getConnection() throws SQLException {
+    return DriverManager.getConnection(url, connectionInfo);
+  }
+
+  public Connection getConnection(String username, String password)
+      throws SQLException {
+    return DriverManager.getConnection(url, username, password);
+  }
+
+  public PrintWriter getLogWriter() {
+    return logWriter;
+  }
+
+  public void setLogWriter(final PrintWriter out) {
+    logWriter = out;
+  }
+
+  public int getLoginTimeout() {
+    return 0;
+  }
+
+  public void setLoginTimeout(int seconds) {
+  }
+
+  public boolean isWrapperFor(Class<?> iface) {
+    return false;
+  }
+
+  public <T> T unwrap(Class<T> iface) throws SQLException {
+    throw new SQLException(getClass().getName() + " wraps nothing");
+  }
+
+  private static synchronized void loadDriver(final String driver)
+      throws SQLException {
+    // I've seen some drivers (*cough* Informix *cough*) which won't load
+    // on multiple threads at the same time. Forcing our code to synchronize
+    // around loading the driver ensures we won't ever ask for the same driver
+    // to initialize from different threads. Of course that could still happen
+    // in other parts of the same JVM, but its quite unlikely.
+    //
+    try {
+      Class.forName(driver, true, threadCL());
+    } catch (ClassNotFoundException err) {
+      final SQLException e;
+      e = new SQLException("Driver class " + driver + " not available");
+      e.initCause(err);
+      throw e;
+    }
+  }
+
+  private static ClassLoader threadCL() {
+    try {
+      return Thread.currentThread().getContextClassLoader();
+    } catch (SecurityException e) {
+      return Database.class.getClassLoader();
+    }
+  }
+}
diff --git a/test/com/google/gwtorm/server/PhoneBookDbTestCase.java b/test/com/google/gwtorm/server/PhoneBookDbTestCase.java
index 32b164d..ba19836 100644
--- a/test/com/google/gwtorm/server/PhoneBookDbTestCase.java
+++ b/test/com/google/gwtorm/server/PhoneBookDbTestCase.java
@@ -21,6 +21,7 @@
 import com.google.gwtorm.data.TestPerson;
 import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.jdbc.SimpleDataSource;
 
 import junit.framework.TestCase;
 
@@ -44,7 +45,7 @@
     final Properties p = new Properties();
     p.setProperty("driver", org.h2.Driver.class.getName());
     p.setProperty("url", "jdbc:h2:mem:PhoneBookDb" + (runCount++));
-    db = new Database<PhoneBookDb>(p, PhoneBookDb.class);
+    db = new Database<PhoneBookDb>(new SimpleDataSource(p), PhoneBookDb.class);
     openSchemas = new ArrayList<PhoneBookDb>();
   }