]> source.dussan.org Git - iciql.git/commitdiff
Implement DAO externalized statement loading based on runtime Mode
authorJames Moger <james.moger@gitblit.com>
Mon, 10 Nov 2014 16:59:09 +0000 (11:59 -0500)
committerJames Moger <james.moger@gitblit.com>
Tue, 11 Nov 2014 02:46:37 +0000 (21:46 -0500)
README.markdown
src/main/java/com/iciql/DaoClasspathStatementProvider.java [new file with mode: 0644]
src/main/java/com/iciql/DaoProxy.java
src/main/java/com/iciql/DaoStatementProvider.java [new file with mode: 0644]
src/main/java/com/iciql/Db.java
src/site/dao.mkd
src/site/index.mkd
src/test/java/com/iciql/test/IciqlSuite.java
src/test/java/com/iciql/test/ProductDaoTest.java
src/test/java/iciql.properties [new file with mode: 0644]

index fdc5699bc582f4ac36387cf0869141b8d8f01fac..62079ce15cde36f67c19f763b271f152d9c1dbeb 100644 (file)
@@ -5,7 +5,7 @@ iciql **is**...
 - a model-based, database access wrapper for JDBC\r
 - for modest database schemas and basic statement generation\r
 - for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety\r
-- small (225KB with debug symbols) with no runtime dependencies\r
+- small (<250KB with debug symbols) with no runtime dependencies\r
 - pronounced *icicle* (although it could be French: *ici ql* - here query language)\r
 - a friendly fork of the H2 [JaQu](http://h2database.com/html/jaqu.html) project\r
 \r
diff --git a/src/main/java/com/iciql/DaoClasspathStatementProvider.java b/src/main/java/com/iciql/DaoClasspathStatementProvider.java
new file mode 100644 (file)
index 0000000..b1bbe14
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2014 James Moger.
+ *
+ * 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.iciql;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+import com.iciql.Iciql.Mode;
+
+/**
+ * Loads DAO statements from Properties resource files the classpath.
+ *
+ * @author James Moger
+ *
+ */
+public class DaoClasspathStatementProvider implements DaoStatementProvider {
+
+       private final Properties externalStatements;
+
+       public DaoClasspathStatementProvider() {
+               externalStatements = load();
+       }
+
+       /**
+        * Returns the list of statement resources to try locating.
+        *
+        * @return
+        */
+       protected String[] getStatementResources() {
+               return new String[] { "/iciql.properties", "/iciql.xml", "/conf/iciql.properties", "/conf/iciql.xml" };
+       }
+
+       /**
+        * Loads the first statement resource found on the classpath.
+        *
+        * @return the loaded statements
+        */
+       private Properties load() {
+
+               Properties props = new Properties();
+               for (String resource : getStatementResources()) {
+
+                       InputStream is = null;
+
+                       try {
+                               is = DaoProxy.class.getResourceAsStream(resource);
+
+                               if (is != null) {
+
+                                       if (resource.toLowerCase().endsWith(".xml")) {
+                                               // load an .XML statements file
+                                               props.loadFromXML(is);
+                                       } else {
+                                               // load a .Properties statements file
+                                               props.load(is);
+                                       }
+
+                                       break;
+                               }
+
+                       } catch (Exception e) {
+                               throw new IciqlException(e, "Failed to parse {0}", resource);
+                       } finally {
+                               try {
+                                       is.close();
+                               } catch (Exception e) {
+                               }
+                       }
+
+               }
+               return props;
+       }
+
+       @Override
+       public String getStatement(String idOrStatement, Mode mode) {
+               final String modePrefix = "%" + mode.name().toLowerCase() + ".";
+               String value = externalStatements.getProperty(idOrStatement, idOrStatement);
+               value = externalStatements.getProperty(modePrefix + idOrStatement, value);
+               return value;
+       }
+
+}
index db5f9119668cb27de5e8bfdda5dc3fb044c6ad4a..cafd6f7fe546d1dc7527eba19a3decf222a1c2c5 100644 (file)
@@ -72,7 +72,7 @@ final class DaoProxy<X extends Dao> implements InvocationHandler, Dao {
         * @return a proxy object
         */
        @SuppressWarnings("unchecked")
-       X buildProxy() {
+       X build() {
 
                if (!daoInterface.isInterface()) {
                        throw new IciqlException("Dao {0} must be an interface!", daoInterface.getName());
@@ -115,12 +115,14 @@ final class DaoProxy<X extends Dao> implements InvocationHandler, Dao {
                        } else if (method.isAnnotationPresent(SqlQuery.class)) {
 
                                String sql = method.getAnnotation(SqlQuery.class).value();
-                               return executeQuery(method, args, sql);
+                               String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode());
+                               return executeQuery(method, args, statement);
 
                        } else if (method.isAnnotationPresent(SqlStatement.class)) {
 
                                String sql = method.getAnnotation(SqlStatement.class).value();
-                               return executeStatement(method, args, sql);
+                               String statement = db.getDaoStatementProvider().getStatement(sql, db.getMode());
+                               return executeStatement(method, args, statement);
 
                        } else {
 
@@ -220,6 +222,11 @@ final class DaoProxy<X extends Dao> implements InvocationHandler, Dao {
 
                                        objects.add(value);
 
+                                       if (!isArray) {
+                                               // we are not returning an array so we break
+                                               // the loop and return the first result
+                                               break;
+                                       }
                                }
 
                        } catch (SQLException e) {
diff --git a/src/main/java/com/iciql/DaoStatementProvider.java b/src/main/java/com/iciql/DaoStatementProvider.java
new file mode 100644 (file)
index 0000000..7e7522a
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2014 James Moger.
+ *
+ * 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.iciql;
+
+import com.iciql.Iciql.Mode;
+
+/**
+ * Defines the interface for retrieving externalized DAO statements.
+ *
+ * @author James Moger
+ *
+ */
+public interface DaoStatementProvider {
+
+       /**
+        * Returns the statement associated with the id.
+        *
+        * @param idOrStatement
+        * @param mode
+        * @return the statement
+        */
+       String getStatement(String idOrStatement, Mode mode);
+}
index 794417e4b50da5fcd62a9790f38f786f7133295c..7413c8f4f8ecdde93217837340b2b539ffe093af 100644 (file)
@@ -74,6 +74,7 @@ public class Db implements AutoCloseable {
 
        private boolean skipCreate;
        private boolean autoSavePoint = true;
+       private DaoStatementProvider daoStatementProvider;
 
        static {
                TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>());
@@ -102,6 +103,7 @@ public class Db implements AutoCloseable {
                }
                dialect = getDialect(databaseName, conn.getClass().getName());
                dialect.configureDialect(this);
+               daoStatementProvider = new NoExternalDaoStatements();
        }
 
        /**
@@ -237,7 +239,30 @@ public class Db implements AutoCloseable {
         */
        @SuppressWarnings("resource")
        public <X extends Dao> X open(Class<X> daoClass) {
-               return new DaoProxy<X>(this, daoClass).buildProxy();
+               return new DaoProxy<X>(this, daoClass).build();
+       }
+
+       /**
+        * Returns the DAO statement provider.
+        *
+        * @return the DAO statement provider
+        */
+       public DaoStatementProvider getDaoStatementProvider() {
+               return daoStatementProvider;
+       }
+
+       /**
+        * Sets the DAO statement provider.
+        *
+        * @param statementProvider
+        */
+       public void setDaoStatementProvider(DaoStatementProvider statementProvider) {
+               if (statementProvider == null) {
+                       throw new IciqlException("You must provide a valid {0} instance!",
+                                       DaoStatementProvider.class.getSimpleName());
+               }
+
+               this.daoStatementProvider = statementProvider;
        }
 
        /**
@@ -841,14 +866,12 @@ public class Db implements AutoCloseable {
        }
 
        /**
-        *
-        * @author James Moger
-        *
+        * Default DAO statement provider.
         */
        class NoExternalDaoStatements implements DaoStatementProvider {
 
                @Override
-               public String getStatement(String idOrStatement) {
+               public String getStatement(String idOrStatement, Mode mode) {
                        return idOrStatement;
                }
 
index 660c7afbffe09cfef2117bd383e8120ec532f7ac..491d08563ba79e0b91c444570cd714acdaabdaa6 100644 (file)
@@ -135,3 +135,46 @@ public interface MyDao extends Dao {
 }\r
 ---JAVA---\r
 \r
+### Runtime Mode & External Statements\r
+\r
+Sometimes you may need to specify a slightly different SQL statement for a database engine you might be using in development but not in production.  For example, you might develop with H2 and deploy with PostgreSQL.\r
+\r
+Being able to switch the DAO statements executed based on the runtime mode would be helpful for some scenarios.  Iciql supports this use-case with a `DaoStatementProvider` and provides three mode options: `DEV`, `TEST`, and `PROD`.\r
+\r
+#### External Statement DAO Example\r
+---JAVA---\r
+public interface MyDao extends Dao {\r
+    @SqlQuery("some.query")\r
+    Product [] getProductsWithRuntimeModeDependentQuery();\r
+}\r
+\r
+Db db = Db.open("jdbc:h2:mem:iciql");\r
+// set a classpath statement resource provider\r
+db.setDaoStatementProvider(new DaoClasspathStatementProvider());\r
+\r
+// open the dao and retrieve the products\r
+MyDao dao = db.open(MyDao.class);\r
+Product [] products = dao.getProductsWithRuntimeModeDependentQuery();\r
+---JAVA---\r
+\r
+#### External Statement Resource Example\r
+\r
+---FIXED---\r
+some.query = select * from Products                                       # default statement\r
+%prod.some.query = select * from Products                                 # will be used in PROD mode\r
+%test.some.query = select * from Products where category = 'Beverages'    # will be used in TEST mode\r
+%dev.some.query = select * from Products where category = 'Condiments'    # will be used in DEV mode\r
+---FIXED---\r
+\r
+#### DaoClasspathStatementProvider\r
+\r
+Iciql ships with one useful implementation of a DaoStatementProvider: `DaoClasspathStatementProvider`.\r
+\r
+This provider will load a single external statement resource from the classpath, if found. It tries to locate one of the following classpath resources and loads the first one identified using the `java.util.Properties` class.\r
+\r
+1. `/iciql.properties`\r
+2. `/iciql.xml`\r
+3. `/conf/iciql.properties`\r
+4. `/conf/iciql.xml`\r
+\r
+Every `@SqlQuery` and `@SqlStatement` method will ask the `DaoStatementProvider` for the statement to execute based on the annotation value and the runtime mode.  For the `DaoClasspathStatementProvider`, if the annotation value is not a key in the resource file it is assumed to be a statement and is returned to the DAO object for execution.  This allows you to externalize a handful of statements - or all of them if you do not want to hard-code anything.
\ No newline at end of file
index b9fc549def006f7a998677b1295223cad49a2df3..1c742f660b30c94ccd8c1b40f6ffddc348485cce 100644 (file)
@@ -5,7 +5,7 @@ iciql **is**...
 - a model-based, database access wrapper for JDBC\r
 - for modest database schemas and basic statement generation\r
 - for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety\r
-- small (225KB) with debug symbols and no runtime dependencies\r
+- small (<250KB) with debug symbols and no runtime dependencies\r
 - pronounced *icicle* (although it could be French: *ici ql* - here query language)\r
 - a friendly fork of the H2 [JaQu][jaqu] subproject\r
 \r
@@ -66,7 +66,7 @@ public interface MyDao extends Dao {
 @TypeAdapter(InvoiceAdapterImpl.class)\r
 public @interface InvoiceAdapter { }\r
 \r
-// Crate a DAO instance with your Db and work more clearly\r
+// Create a DAO instance with your Db and work more clearly\r
 try (Db db = Db.open("jdbc:h2:mem:iciql")) {\r
 \r
     MyDao dao = db.open(MyDao.class);\r
@@ -91,6 +91,10 @@ This is very useful for mapping your field domain models to SQL without having t
 \r
 You might use this to take advantage of the underlying database's type system.  For example, PostgreSQL ships with the compelling JSON/JSONB/XML data types.  Iciql provides String and Object adapters to facilitate use of those data types.\r
 \r
+### runtime mode support\r
+\r
+Mode support allows you to tweak the behavior of your `@TypeAdapter` and `DAO` implementations to adapt to runtime conditions such as developing on a different database than you deploy on.\r
+\r
 ### Supported Databases (Unit-Tested)\r
 - [H2](http://h2database.com) ${h2.version}\r
 - [HSQLDB](http://hsqldb.org) ${hsqldb.version}\r
index c088ff9c628cfbedec46038fa1a66e363236c80e..c80da93dc1a7ee0219b9b07042252bdcaad0dbbf 100644 (file)
@@ -48,6 +48,7 @@ import com.beust.jcommander.ParameterException;
 import com.beust.jcommander.Parameters;\r
 import com.iciql.Constants;\r
 import com.iciql.Db;\r
+import com.iciql.Iciql.Mode;\r
 import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest;\r
 import com.iciql.test.models.BooleanModel;\r
 import com.iciql.test.models.CategoryAnnotationOnly;\r
@@ -144,13 +145,18 @@ public class IciqlSuite {
                return Math.abs(expected - actual) <= 0.000001d;\r
        }\r
 \r
+       public static Db openNewDb() {\r
+               return openNewDb(Mode.PROD);\r
+       }\r
+\r
        /**\r
         * Open a new Db object. All connections are cached and re-used to eliminate\r
         * embedded database startup costs.\r
         *\r
+        * @param mode\r
         * @return a fresh Db object\r
         */\r
-       public static Db openNewDb() {\r
+       public static Db openNewDb(Mode mode) {\r
                String testUrl = System.getProperty("iciql.url", DEFAULT_TEST_DB.url);\r
                String testUser = System.getProperty("iciql.user", DEFAULT_TEST_DB.username);\r
                String testPassword = System.getProperty("iciql.password", DEFAULT_TEST_DB.password);\r
@@ -168,7 +174,7 @@ public class IciqlSuite {
                        dataSources.put(testUrl, dataSource);\r
                        connectionFactories.put(testUrl, factory);\r
                }\r
-               db = Db.open(dataSource);\r
+               db = Db.open(dataSource, mode);\r
 \r
                // drop views\r
                db.dropView(ProductView.class);\r
index 78987fdb0d08f9e6b2ef9e8dba11724444c8bb81..e5549c16e06803e6bbf6be5af49b71fccd0fb504 100644 (file)
@@ -26,7 +26,9 @@ import org.junit.Before;
 import org.junit.Test;
 
 import com.iciql.Dao;
+import com.iciql.DaoClasspathStatementProvider;
 import com.iciql.Db;
+import com.iciql.Iciql.Mode;
 import com.iciql.IciqlException;
 import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest;
 import com.iciql.test.DataTypeAdapterTest.SupportedTypesAdapter;
@@ -48,6 +50,7 @@ public class ProductDaoTest extends Assert {
                db = IciqlSuite.openNewDb();
                db.insertAll(Product.getList());
                db.insertAll(Order.getList());
+               db.setDaoStatementProvider(new DaoClasspathStatementProvider());
        }
 
        @After
@@ -273,6 +276,47 @@ public class ProductDaoTest extends Assert {
                assertTrue(obj1.equivalentTo(obj2));
        }
 
+       @Test
+       public void testDefaultProdResourceQueryReturnModels() {
+
+               ProductDao dao = db.open(ProductDao.class);
+
+               Product[] products = dao.getProductsFromResourceQuery();
+               assertEquals(10, products.length);
+       }
+
+       @Test
+       public void testDevResourceQueryReturnModels() {
+
+               Db db = IciqlSuite.openNewDb(Mode.DEV);
+               db.insertAll(Product.getList());
+               db.insertAll(Order.getList());
+               db.setDaoStatementProvider(new DaoClasspathStatementProvider());
+
+               ProductDao dao = db.open(ProductDao.class);
+
+               Product[] products = dao.getProductsFromResourceQuery();
+               assertEquals(5, products.length);
+
+               db.close();
+       }
+
+       @Test
+       public void testTestResourceQueryReturnModels() {
+
+               Db db = IciqlSuite.openNewDb(Mode.TEST);
+               db.insertAll(Product.getList());
+               db.insertAll(Order.getList());
+               db.setDaoStatementProvider(new DaoClasspathStatementProvider());
+
+               ProductDao dao = db.open(ProductDao.class);
+
+               Product[] products = dao.getProductsFromResourceQuery();
+               assertEquals(2, products.length);
+
+               db.close();
+       }
+
        /**
         * Define the Product DAO interface.
         */
@@ -320,7 +364,8 @@ public class ProductDaoTest extends Assert {
                @SqlQuery("select productId from Product where category = :category")
                long[] getProductIdsForCategory(@Bind("category") String cat);
 
-               @SqlQuery("select orderDate from Orders order by orderDate desc limit 1")
+               // will break ResultSet iteration after retrieving first value
+               @SqlQuery("select orderDate from Orders order by orderDate desc")
                Date getMostRecentOrder();
 
                @SqlStatement("update Product set productName = 'test' where productId = 1")
@@ -335,12 +380,16 @@ public class ProductDaoTest extends Assert {
                @SqlStatement("update Product set category = :newCategory where category = :oldCategory")
                int renameProductCategoryReturnsCount(@Bind("oldCategory") String oldCategory, @Bind("newCategory") String newCategory);
 
-               @SqlQuery("select obj from dataTypeAdapters limit 1")
+               // will break ResultSet iteration after retrieving first value
+               @SqlQuery("select obj from dataTypeAdapters")
                @SupportedTypesAdapter
                SupportedTypes getCustomDataType();
 
                @SqlStatement("update dataTypeAdapters set obj=:2 where id=:1")
                boolean setSupportedTypes(long id, @SupportedTypesAdapter SupportedTypes obj);
 
+               @SqlQuery("get.products")
+               Product[] getProductsFromResourceQuery();
+
        }
 }
diff --git a/src/test/java/iciql.properties b/src/test/java/iciql.properties
new file mode 100644 (file)
index 0000000..cecb056
--- /dev/null
@@ -0,0 +1,7 @@
+#
+# Example resource file for DaoClasspathStatementProvider
+#
+
+get.products = select * from Product
+%test.get.products = select * from Product where category = 'Beverages'
+%dev.get.products = select * from Product where category = 'Condiments'