Shantanu Kumar avatar Shantanu Kumar committed 4c72feb

add feature: create datasource based on properties file configuration

Comments (0)

Files changed (7)

 
 # Changes and TODO
 
-- [TODO] Test-cases for untested databases:
-  Oracle, IBM DB2, SQL Server, Sybase
+- [TODO] Test-cases for untested databases: Oracle, IBM DB2, SQL Server, Sybase
 - [TODO] Support Excel files using JDBC driver
 - [TODO] Support XADataSource - maybe use JOTM: http://jotm.objectweb.org/
 
 
+## 0.5 / 2011-Mar-??
+
+- Support for loading datasource configuration from properties file
+- Rename internal function `create-datasource` to `make-datasource`
+
+
 ## 0.4 / 2011-Jan-05
 
 - Clojure 1.3 compatibility
 -*- markdown -*-
 
-# Clj-DBCP v0.4
+# Clj-DBCP v0.5-SNAPSHOT
 
 Clj-DBCP is a simple Java-6/Clojure wrapper around the Apache DBCP library for
 creating database connection pools and for embedding databases in applications.
   (b) Apache Derby, HSQLDB, H2, SQLite
   (c) CUBRID, Firebird, MySQL, PostgreSQL
 * Supported but not tested: Oracle, IBM DB2, jTDS (SQL Server, Sybase)
+* Create datasource as per configuration in Properties file
 
 
 ## Usage
         ;; set minimum and maximum number of idle connections on the datasource
         (set-min-max-idle! ds 3 8)
         ds))
+
+
+## Create DataSource from `properties` file
+
+You can create a DataSource from a properties file
+[http://en.wikipedia.org/wiki/Properties_file](http://en.wikipedia.org/wiki/Properties_file)
+called `clj-dbcp.properties` either in classpath or on the filesystem in
+current directory:
+
+    (make-datasource) ; loads file clj-dbcp.properties and creates DataSource
+
+The file `clj-dbcp.properties` can look like this:
+
+    # Database configuration for project `foo`
+    # There are 5 profiles - dev, dev2, dev3, qa, weird
+    # If you need a new profile, create one and set default to that profile
+    
+    default=dev2
+    
+    dev.db-type=mysql
+    dev.hostport=12.34.56.78:3306
+    dev.database=organisation
+    dev.username=orguser
+    dev.password=d0ntsh4re
+    
+    # In-memory H2 database (very fast, no config required, use for testing)
+    dev2.db-type=h2-memory
+    
+    # On-filesystem H2 database (fast, data available across app restarts)
+    dev3.db-type=h2-filesystem
+    # `db-path` is optional (if not specified, assumed "default")
+    dev3.db-path=/home/eddie/sample
+    
+    # Example of using ODBC DSN for creating datasource
+    mydev.odbc-dsn=cubrid-bituf
+    # Username and password may be optional (subject to ODBC driver)
+    #mydev.username=
+    #mydev.password=
+    
+    # Example of unsupported databases (using JDBC URL)
+    weird.classname=com.weird.MagicDriver
+    weird.jdbc-url=jdbc:weird://34.56.78.90:1234/orgmaster
+    weird.username=Magic_user
+    weird.password=simp1e
   <groupId>org.bituf</groupId>
   <artifactId>clj-dbcp</artifactId>
   <packaging>jar</packaging>
-  <version>0.4</version>
+  <version>0.5-SNAPSHOT</version>
   <name>Clj-DBCP</name>
   <description>
     Clj-DBCP is a simple Java-6/Clojure wrapper around the Apache DBCP library

src/main/clj/org/bituf/clj_dbcp.clj

 (ns org.bituf.clj-dbcp
   (:import
-    (java.sql DriverManager)
-    (org.bituf.clj_dbcp ConnectionWrapper))
-  (:import javax.sql.DataSource)
-  (:import org.apache.commons.dbcp.BasicDataSource))
+    (java.io   Reader)
+    (java.sql  DriverManager)
+    (javax.sql DataSource)
+    (java.util Properties)
+    (org.bituf.clj_dbcp      ConnectionWrapper)
+    (org.apache.commons.dbcp BasicDataSource))
+  (:require
+    [clojure.java.io :as io]
+    [clojure.pprint  :as pp]))
 
 
 (def ^{:doc "Boolean flag (default false) - prints the JDBC URL if true"
   datasource)
 
 
-(defn create-datasource
+(defn make-datasource
   "Create basic data source (instance of the BasicDataSource class).
   Arguments:
     classname         (String) the fully qualified driver classname
       (.setPassword password)
       (.setUrl url)))
   ([datasource-args] ; 1 arg
-    (create-datasource
+    (make-datasource
       (:classname datasource-args)
       (:url       datasource-args)
       (:username  datasource-args)
     derby-memory-datasource
     derby-args"
   [db-protocol db-path]
-  (create-datasource
+  (make-datasource
     (derby-args db-protocol db-path)))
 
 
   "Create an H2 data source
   See also: h2-args"
   [db-protocol db-path]
-  (create-datasource
+  (make-datasource
     (h2-args db-protocol db-path)))
 
 
   "Create an HSQLDB/HyperSQL data source.
   See also: hsql-args"
   ([db-protocol db-path]
-    (create-datasource
+    (make-datasource
       (hsql-args db-protocol db-path)))
   ([]
     (hsql-datasource "mem" default-db-name)))
   [db-host:port db-name username password]
   (let [args (mysql-args
                db-host:port db-name username password)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "SELECT 1;")))
 
   [db-host:port db-name username password]
   (let [args (pgsql-args
                db-host:port db-name username password)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "SELECT version();")))
 
   [db-host:port system-id username password]
   (let [args (oracle-args
                db-host:port system-id username password)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "SELECT 1 FROM DUAL;")))
 
   [db-host:port db-name username password]
   (let [args (db2-args
                db-host:port db-name username password)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "select * from sysibm.SYSDUMMY1;")))
 
                server-type
                db-host:port db-name username password
                properties)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "select 1;")))
 
   "Create an SQLite data source.
   See also: sqlite-args"
   ([db-path]
-    (create-datasource (sqlite-args db-path)))
+    (make-datasource (sqlite-args db-path)))
   ([]
     (sqlite-datasource ":memory:")))
 
   [db-host:port db-name username password]
   (let [args (cubrid-args
                db-host:port db-name username password)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "SELECT 1;")))
 
   [db-host:port db-path username password]
   (let [args (firebird-args
                db-host:port db-path username password)
-        ds   (create-datasource args)]
+        ds   (make-datasource args)]
     (set-validation-query! ds
       "SELECT CAST(1 AS INTEGER) FROM rdb$database;")))
 
   ([odbc-dsn username password properties]
     (let [args (odbc-args
                  odbc-dsn username password properties)
-          ds   (create-datasource args)]
+          ds   (make-datasource args)]
       (set-validation-query! ds
         "SELECT 1;")
       ds))
         url)))
   ([^String dsn]
     (lite-odbc-datasource dsn nil)))
+
+
+;; ========== Loading DataSource from property files params ==========
+
+
+(def ^{:doc "Path to the Properties file (in classpath or filesystem)"
+       :dynamic true}
+      *properties-file* "clj-dbcp.properties")
+
+
+(defn load-properties
+  "Load properties-file `file` (or *properties-file* if not specified) from
+  classpath or filesystem. If the file is found in classpath, then filesystem is
+  not looked up. Properties are returned as a map of property string names to
+  string values."
+  ([file] {:post [(and (map? %)
+                    (every? string? (keys %))
+                    (every? string? (vals %)))]}
+    (let [pfile  (or (io/resource file) file)
+          reader (io/reader pfile)
+          props  (Properties.)]
+      (.load ^Properties props ^Reader reader)
+      (into {} props)))
+  ([] {:pre [(string? *properties-file*)]}
+    (load-properties *properties-file*)))
+
+
+(defn default-profile
+  "Return the default profile name from properties. You should explicitly
+  declare it in the properties file, for example:
+    default=staging
+  If no default profile is specified, then `dev` is assumed after verifying
+  that the dev profile exists in the properties.
+  See also: load-properties"
+  [props-map] {:pre [(and (map? props-map)
+                       (every? string? (keys props-map))
+                       (every? string? (vals props-map)))]}
+  (or (get props-map "default")
+    (and (some #(and (.startsWith ^String % "dev.") (> (count %) 4))
+           (keys props-map)) "dev")
+    (throw (IllegalArgumentException.
+             "No default profile found, nor exists `dev` fallback profile."))))
+
+
+(defn read-profile-properties
+  "Read properties for the given profile (default profile is looked up if none
+  specified.) The returned map is keyword to string-value map, and has the keys
+  stripped of the profile name."
+  [props-map ^String profile] {:post [(map? %)
+                                       (every? keyword? (keys %))
+                                       (every? string?  (vals %))]
+                               :pre  [(map? props-map)
+                                      (string? profile)]}
+  (let [pf (str profile \.)
+        pc (count pf)
+        ks (filter #(and (.startsWith ^String % pf)
+                      (> (count %) pc))
+             (keys props-map))
+        vs (map #(get props-map %) ks)]
+    (zipmap (map #(keyword (.substring ^String % pc)) ks) vs)))
+
+
+(defn on-memory
+  "Helper function to create datasource (on memory) with default configuration."
+  [factory-fn props]
+  (factory-fn))
+
+
+(defn on-filesystem
+  "Helper function to create datasource (on filesystem) as per the arguments
+  passed."
+  [factory-fn props]
+  (let [db-path (:db-path props)]
+    (if db-path
+      (factory-fn db-path)
+      (factory-fn))))
+
+
+(defn on-network
+  "Helper function to create datasource (that connects over network) after
+  verifying that sufficient arguments are passed."
+  [factory-fn props] {:post [(instance? DataSource %)]
+                      :pre  [(fn? factory-fn)
+                             (map? props)
+                             (contains? props :hostport)
+                             (contains? props :database)]}
+  (when (not (and (contains? props :hostport)
+               (contains? props :database)))
+    (throw (IllegalArgumentException.
+             (format "Expected arguments: mandatory - `hostport` and `database`,
+optional (in H2, HSQL, Derby etc) - `username`and `password` but found %s"
+               (with-out-str (pp/pprint props))))))
+  (let [hostport (:hostport props)
+        database (:database props)
+        username (:username props)
+        password (:password props)]
+    (if username
+      (factory-fn hostport database username password)
+      (factory-fn hostport database))))
+
+
+(defn on-odbc
+  "Create DataSource using ODBC DSN."
+  [props]
+  (let [odbc-dsn (:odbc-dsn props)
+        username (:username props)
+        password (:password props)]
+    (when (or (not (string? odbc-dsn))
+            (empty? odbc-dsn))
+      (throw (IllegalArgumentException.
+               (format "Expected `odbc-dsn` key but found %s"
+                 (with-out-str (pp/pprint props))))))
+    (if (contains? props username)
+      (odbc-datasource odbc-dsn username password)
+      (odbc-datasource odbc-dsn))))
+
+
+(defn on-jdbc
+  "Create simple DataSource based on JDBC parameters."
+  [props]
+  (let [classname (:classname props)
+        jdbc-url (:jdbc-url props)
+        username (:username props)
+        password (:password props)]
+    (when (not (every? string? [classname jdbc-url username password]))
+      (throw (IllegalArgumentException.
+               (format "Expected `classname`, `jdbc-url`, `username` and
+`password` keys but found %s"
+                 (with-out-str (pp/pprint props))))))
+    (make-datasource
+      classname jdbc-url username password)))
+
+
+(def ^{:doc "Map of supported databases and respective datasource factory fns"}
+      supported-dbs
+  {:derby-memory       (partial on-memory derby-memory-datasource)
+   :h2-memory          (partial on-memory h2-memory-datasource)
+   :hsql-memory        (partial on-memory hsql-memory-datasource)
+   :sqlite-memory      (partial on-memory sqlite-memory-datasource)
+   ;; filesystem
+   :derby-filesystem   (partial on-filesystem derby-filesystem-datasource)
+   :h2-filesystem      (partial on-filesystem h2-filesystem-datasource)
+   :hsql-filesystem    (partial on-filesystem hsql-filesystem-datasource)
+   :sqlite-filesystem  (partial on-filesystem sqlite-filesystem-datasource)
+   ;; network
+   :derby-network      (partial on-network derby-network-datasource)
+   :h2-network         (partial on-network h2-network-datasource)
+   :hsql-network       (partial on-network hsql-network-datasource)
+   ;; regular, open source, network-only databases
+   :cubrid             (partial on-network cubrid-datasource)
+   :firebird           (partial on-network firebird-datasource)
+   :mysql              (partial on-network mysql-datasource)
+   :pgsql              (partial on-network pgsql-datasource)
+   ;; regular, proprietary, network-only databases
+   :oracle             (partial on-network oracle-datasource)
+   :db2                (partial on-network db2-datasource)
+   :sqlserver          (partial on-network sqlserver-datasource)
+   :sybase             (partial on-network sybase-datasource)
+   })
+
+
+(defn load-datasource-args
+  "Load a map of string names and string values from property file for the
+  given/default profile."
+  ([^String profile] {:post [(map? %)]
+                      :pre  [(string? profile)]}
+    (read-profile-properties
+      (load-properties) profile))
+  ([] {:post [(map? %)]}
+    (load-datasource-args (default-profile (load-properties)))))
+
+
+(defn ^DataSource make-datasource-from-properties
+  "Load DataSource-building properties and create a DataSource as per those."
+  ([props] {:post [(instance? DataSource %)]
+           :pre  [(map? props)
+                  (every? keyword? (keys props))
+                  (every? string?  (vals props))]}
+    (cond
+      ;; supported databases
+      (and (contains? props :db-type)
+          (contains? supported-dbs
+            (keyword
+              (:db-type props))))        ((get supported-dbs (keyword
+                                                               (:db-type props)))
+                                        props)
+      ;; JDBC-ODBC bridge
+      (contains? props :odbc-dsn)     (on-odbc props)
+      ;; JDBC Driver
+      (contains? props :jdbc-url)     (on-jdbc props)
+      ;; Else
+      :else                           (throw
+                                        (IllegalArgumentException.
+                                          (format
+                                            "Expected either of keys `db-type`,
+`odbc-dsn` and `jdbc-url` but found %s Valid db-type values are: %s"
+                                            (with-out-str (pp/pprint props))
+                                            (with-out-str
+                                              (pp/pprint
+                                                (keys supported-dbs))))))))
+  ([] {:post [(instance? DataSource %)]}
+    (let [props (load-datasource-args)]
+      (make-datasource-from-properties props))))

src/test/clj/clj-dbcp.properties

+# This properties file is in classpath
+
+default=mydev
+
+mydev.odbc-dsn=cubrid-bituf
+#mydev.username=
+#mydev.password=
+
+dev2.db-type=h2-memory
+
+dev3.db-type=h2-filesystem
+
+# MySQL profile
+# hostport can also be in the form "localhost:3306"
+mysql.db-type=mysql
+mysql.hostport=localhost
+mysql.database=bituf
+mysql.username=root
+mysql.password=root
+
+# CUBRID credentials
+# hostport is supposed to be in the form "localhost:33000"
+jdbc.classname=org.h2.Driver
+jdbc.jdbc-url=jdbc:h2:mem:sample
+jdbc.username=sa
+jdbc.password=

src/test/clj/org/bituf/test_clj_dbcp.clj

 (ns org.bituf.test-clj-dbcp
   (:require
-    [org.bituf.test-util :as tu])
+    [org.bituf.test-util :as tu]
+    [clojure.pprint :as pp])
   (:use org.bituf.clj-dbcp)
   (:use org.bituf.dbserver-util)
   (:use [clojure.contrib.sql :as sql :only ()])
       ds)))
 
 
+(defn test-datasource-from-properties-file-helper
+  []
+  (doseq [[msg each] [["Default" (make-datasource-from-properties)]
+                      ["mydev" (make-datasource-from-properties (load-datasource-args "mydev"))]
+                      ["dev2"  (make-datasource-from-properties (load-datasource-args "dev2"))]
+                      ["dev3"  (make-datasource-from-properties (load-datasource-args "dev3"))]
+                      ["mysql" (make-datasource-from-properties (load-datasource-args "mysql"))]
+                      ["jdbc"  (make-datasource-from-properties (load-datasource-args "jdbc"))]]]
+    (testing msg
+      (test-crud (db-spec each)))))
+
+
+(deftest test-datasource-from-properties-file
+  (let [p (load-properties)]
+    (pp/pprint p)
+    (is (not (empty? p))))
+  (test-datasource-from-properties-file-helper)
+  (binding [*properties-file* "test-clj-dbcp.properties"]
+    (test-datasource-from-properties-file-helper)))
+
+
 (defn test-ns-hook []
   (test-odbc)
   (test-derby)
   (test-pgsql)
   (smoketest-untested-datasources)
   (test-jndi)
-  (test-custom-datasource-parameters))
+  (test-custom-datasource-parameters)
+  (test-datasource-from-properties-file))

test-clj-dbcp.properties

+# This properties file is in classpath
+
+default=mydev
+
+mydev.odbc-dsn=cubrid-bituf
+#mydev.username=
+#mydev.password=
+
+dev2.db-type=h2-memory
+
+dev3.db-type=h2-filesystem
+
+# MySQL profile
+# hostport can also be in the form "localhost:3306"
+mysql.db-type=mysql
+mysql.hostport=localhost
+mysql.database=bituf
+mysql.username=root
+mysql.password=root
+
+# CUBRID credentials
+# hostport is supposed to be in the form "localhost:33000"
+jdbc.classname=org.h2.Driver
+jdbc.jdbc-url=jdbc:h2:mem:sample
+jdbc.username=sa
+jdbc.password=
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.