diff --git a/src/main/java/com/threerings/util/ConnectionProviderUtil.java b/src/main/java/com/threerings/util/ConnectionProviderUtil.java
new file mode 100644
index 0000000..9fb3d10
--- /dev/null
+++ b/src/main/java/com/threerings/util/ConnectionProviderUtil.java
@@ -0,0 +1,73 @@
+//
+// ooo-util - a place for OOO utilities
+// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved
+// http://github.com/threerings/ooo-util/blob/master/LICENSE
+
+package com.threerings.util;
+
+import java.util.Properties;
+
+import com.samskivert.util.Config;
+
+import com.samskivert.jdbc.ConnectionProvider;
+import com.samskivert.jdbc.StaticConnectionProvider;
+
+public class ConnectionProviderUtil
+{
+ public enum DB {
+ postgres, hsqldb;
+ }
+
+ /**
+ * Creates a ConnectionProvider for the type of databases specified by db.type in
+ * config.
+ * Types
+ *
+ * - postgres
+ * - Uses {@link PostgresUtil#createPoolingProvider(Config, String)} with the given config
+ * and ident.
+ * - hsqldb
+ * - a {@link StaticConnectionProvider} is created from
+ * the sub-properties under
db. It defaults to an in-memory database if
+ * db.default.url isn't defined.
+ *
+ *
+ *
+ * @throws IllegalArgumentException - if db.type doesn't match a known type.
+ */
+ public static ConnectionProvider createProvider (Config config, String ident)
+ {
+ return createProvider(config, ident, "db");
+ }
+
+ /**
+ * Creates a ConnectionProvider as in {@link #createProvider(Config,String)}, but uses
+ * the supplied {@code prefix} in place of {@code db}.
+ */
+ public static ConnectionProvider createProvider (Config config, String ident, String prefix)
+ {
+ DB type = DB.valueOf(config.getValue(prefix + ".type", "postgres"));
+ switch(type) {
+ case postgres:
+ return PostgresUtil.createPoolingProvider(config, ident, prefix);
+ case hsqldb:
+ Properties sub = config.getSubProperties(prefix);
+ putIfNotPresent(sub, "default.driver", "org.hsqldb.jdbcDriver");
+ putIfNotPresent(sub, "default.url", "jdbc:hsqldb:mem:.");
+ putIfNotPresent(sub, "default.username", "sa");
+ putIfNotPresent(sub, "default.password", "");
+ return new StaticConnectionProvider(sub);
+ default:
+ throw new IllegalArgumentException("Unknown db type " +
+ config.getValue(prefix + ".type", (String)null));
+ }
+ }
+
+ protected static void putIfNotPresent (Properties props, String key, String value)
+ {
+ if (!props.containsKey(key)) {
+ props.put(key, value);
+ }
+ }
+
+}
diff --git a/src/main/java/com/threerings/util/PostgresUtil.java b/src/main/java/com/threerings/util/PostgresUtil.java
new file mode 100644
index 0000000..e9cf46b
--- /dev/null
+++ b/src/main/java/com/threerings/util/PostgresUtil.java
@@ -0,0 +1,105 @@
+//
+// ooo-util - a place for OOO utilities
+// Copyright (C) 2011 Three Rings Design, Inc., All Rights Reserved
+// http://github.com/threerings/ooo-util/blob/master/LICENSE
+
+package com.threerings.util;
+
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.sql.DataSource;
+
+import org.postgresql.jdbc2.optional.PoolingDataSource;
+
+import com.samskivert.jdbc.ConnectionProvider;
+import com.samskivert.jdbc.DataSourceConnectionProvider;
+import com.samskivert.util.Config;
+import com.samskivert.util.MissingPropertyException;
+
+import static com.samskivert.util.PropertiesUtil.requireProperty;
+
+/**
+ * Postgres related utility methods.
+ */
+public class PostgresUtil
+{
+ /**
+ * Creates a ConnectionProvider that communicates with a Postgres database server using
+ * read-only and read-write connections via a connection pooled data source. The supplied
+ * config must provide the following configuration:
+ *
+ * db.default.server = DBHOST
+ * db.default.port = 5432
+ * db.default.database = DATABASE
+ * db.default.username = USERNAME
+ * db.default.password = PASSWORD
+ * db.readonly.maxconns = 1
+ * db.readwrite.maxconns = 1
+ *
+ * It can optionally provide overrides for the server, port, database, username and password
+ * fields for the readonly and readwrite conncetions:
+ *
+ * db.readonly.server = READSLAVE
+ * db.readonly.username = READUSER
+ * db.readonly.password = READPASS
+ * db.readwrite.server = MASTER
+ * ...
+ *
+ *
+ * @param config used to obtain the configuration data.
+ * @param projectId a unique identifier for the system creating this provider which will be
+ * used to prefix the identifiers of the data source names (which are JVM global).
+ */
+ public static ConnectionProvider createPoolingProvider (Config config, String projectId)
+ {
+ return createPoolingProvider(config, projectId, "db");
+ }
+
+ /**
+ * Creates a ConnectionProvider as in {@link #createPoolingProvider(Config,String)}, but uses
+ * the supplied {@code prefix} in place of {@code db}.
+ */
+ public static ConnectionProvider createPoolingProvider (
+ Config config, String projectId, String prefix)
+ {
+ final DataSource[] sources = new DataSource[2];
+ String[] modes = new String[] { "readonly", "readwrite" };
+ String defPrefix = prefix + ".default";
+ for (int ii = 0; ii < sources.length; ii++) {
+ String modePrefix = prefix + "." + modes[ii];
+ try {
+ // start with defaults, then apply overrides
+ Properties props = config.getSubProperties(defPrefix);
+ config.getSubProperties(modePrefix, props);
+ PoolingDataSource source = new PoolingDataSource();
+ source.setDataSourceName(projectId + "." + modes[ii]);
+ source.setServerName(requireProperty(props, "server"));
+ source.setDatabaseName(requireProperty(props, "database"));
+ source.setPortNumber(Integer.parseInt(requireProperty(props, "port")));
+ source.setUser(requireProperty(props, "username"));
+ source.setPassword(requireProperty(props, "password"));
+ source.setMaxConnections(Integer.parseInt(props.getProperty("maxconns", "1")));
+ sources[ii] = source;
+ } catch (MissingPropertyException mpe) {
+ String key = mpe.getKey();
+ throw new MissingPropertyException(
+ key, "Unable to locate required property '" + key + "' as '" +
+ defPrefix + "." + key + "' or '" + modePrefix + "." + key + "'.");
+ }
+ }
+ return new DataSourceConnectionProvider("jdbc:postgresql", sources[0], sources[1]) {
+ @Override public void shutdown () {
+ if (_shutdown.getAndSet(true)) {
+ // Only shutdown the sources once; Postgres' pooling data source doesn't like
+ // multiple shutdown calls
+ return;
+ }
+ for (DataSource source : sources) {
+ ((PoolingDataSource)source).close();
+ }
+ }
+ protected AtomicBoolean _shutdown = new AtomicBoolean();
+ };
+ }
+}