summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2014-11-10 11:59:09 -0500
committerJames Moger <james.moger@gitblit.com>2014-11-10 21:46:37 -0500
commita6df2de41953e10db1527e54acd734c0f0a1fa28 (patch)
treead3a86dc2a54cf3ac9418e0e5e86f396f4e1dfb0
parentc1d81bcdfc948b417964c6b69be2ee5801e5e1c9 (diff)
downloadiciql-a6df2de41953e10db1527e54acd734c0f0a1fa28.tar.gz
iciql-a6df2de41953e10db1527e54acd734c0f0a1fa28.zip
Implement DAO externalized statement loading based on runtime Mode
-rw-r--r--README.markdown2
-rw-r--r--src/main/java/com/iciql/DaoClasspathStatementProvider.java95
-rw-r--r--src/main/java/com/iciql/DaoProxy.java13
-rw-r--r--src/main/java/com/iciql/DaoStatementProvider.java36
-rw-r--r--src/main/java/com/iciql/Db.java33
-rw-r--r--src/site/dao.mkd43
-rw-r--r--src/site/index.mkd8
-rw-r--r--src/test/java/com/iciql/test/IciqlSuite.java10
-rw-r--r--src/test/java/com/iciql/test/ProductDaoTest.java53
-rw-r--r--src/test/java/iciql.properties7
10 files changed, 285 insertions, 15 deletions
diff --git a/README.markdown b/README.markdown
index fdc5699..62079ce 100644
--- a/README.markdown
+++ b/README.markdown
@@ -5,7 +5,7 @@ iciql **is**...
- a model-based, database access wrapper for JDBC
- for modest database schemas and basic statement generation
- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety
-- small (225KB with debug symbols) with no runtime dependencies
+- small (<250KB with debug symbols) with no runtime dependencies
- pronounced *icicle* (although it could be French: *ici ql* - here query language)
- a friendly fork of the H2 [JaQu](http://h2database.com/html/jaqu.html) project
diff --git a/src/main/java/com/iciql/DaoClasspathStatementProvider.java b/src/main/java/com/iciql/DaoClasspathStatementProvider.java
new file mode 100644
index 0000000..b1bbe14
--- /dev/null
+++ b/src/main/java/com/iciql/DaoClasspathStatementProvider.java
@@ -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;
+ }
+
+}
diff --git a/src/main/java/com/iciql/DaoProxy.java b/src/main/java/com/iciql/DaoProxy.java
index db5f911..cafd6f7 100644
--- a/src/main/java/com/iciql/DaoProxy.java
+++ b/src/main/java/com/iciql/DaoProxy.java
@@ -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
index 0000000..7e7522a
--- /dev/null
+++ b/src/main/java/com/iciql/DaoStatementProvider.java
@@ -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);
+}
diff --git a/src/main/java/com/iciql/Db.java b/src/main/java/com/iciql/Db.java
index 794417e..7413c8f 100644
--- a/src/main/java/com/iciql/Db.java
+++ b/src/main/java/com/iciql/Db.java
@@ -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;
}
diff --git a/src/site/dao.mkd b/src/site/dao.mkd
index 660c7af..491d085 100644
--- a/src/site/dao.mkd
+++ b/src/site/dao.mkd
@@ -135,3 +135,46 @@ public interface MyDao extends Dao {
}
---JAVA---
+### Runtime Mode & External Statements
+
+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.
+
+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`.
+
+#### External Statement DAO Example
+---JAVA---
+public interface MyDao extends Dao {
+ @SqlQuery("some.query")
+ Product [] getProductsWithRuntimeModeDependentQuery();
+}
+
+Db db = Db.open("jdbc:h2:mem:iciql");
+// set a classpath statement resource provider
+db.setDaoStatementProvider(new DaoClasspathStatementProvider());
+
+// open the dao and retrieve the products
+MyDao dao = db.open(MyDao.class);
+Product [] products = dao.getProductsWithRuntimeModeDependentQuery();
+---JAVA---
+
+#### External Statement Resource Example
+
+---FIXED---
+some.query = select * from Products # default statement
+%prod.some.query = select * from Products # will be used in PROD mode
+%test.some.query = select * from Products where category = 'Beverages' # will be used in TEST mode
+%dev.some.query = select * from Products where category = 'Condiments' # will be used in DEV mode
+---FIXED---
+
+#### DaoClasspathStatementProvider
+
+Iciql ships with one useful implementation of a DaoStatementProvider: `DaoClasspathStatementProvider`.
+
+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.
+
+1. `/iciql.properties`
+2. `/iciql.xml`
+3. `/conf/iciql.properties`
+4. `/conf/iciql.xml`
+
+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
diff --git a/src/site/index.mkd b/src/site/index.mkd
index b9fc549..1c742f6 100644
--- a/src/site/index.mkd
+++ b/src/site/index.mkd
@@ -5,7 +5,7 @@ iciql **is**...
- a model-based, database access wrapper for JDBC
- for modest database schemas and basic statement generation
- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety
-- small (225KB) with debug symbols and no runtime dependencies
+- small (<250KB) with debug symbols and no runtime dependencies
- pronounced *icicle* (although it could be French: *ici ql* - here query language)
- a friendly fork of the H2 [JaQu][jaqu] subproject
@@ -66,7 +66,7 @@ public interface MyDao extends Dao {
@TypeAdapter(InvoiceAdapterImpl.class)
public @interface InvoiceAdapter { }
-// Crate a DAO instance with your Db and work more clearly
+// Create a DAO instance with your Db and work more clearly
try (Db db = Db.open("jdbc:h2:mem:iciql")) {
MyDao dao = db.open(MyDao.class);
@@ -91,6 +91,10 @@ This is very useful for mapping your field domain models to SQL without having t
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.
+### runtime mode support
+
+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.
+
### Supported Databases (Unit-Tested)
- [H2](http://h2database.com) ${h2.version}
- [HSQLDB](http://hsqldb.org) ${hsqldb.version}
diff --git a/src/test/java/com/iciql/test/IciqlSuite.java b/src/test/java/com/iciql/test/IciqlSuite.java
index c088ff9..c80da93 100644
--- a/src/test/java/com/iciql/test/IciqlSuite.java
+++ b/src/test/java/com/iciql/test/IciqlSuite.java
@@ -48,6 +48,7 @@ import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.iciql.Constants;
import com.iciql.Db;
+import com.iciql.Iciql.Mode;
import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest;
import com.iciql.test.models.BooleanModel;
import com.iciql.test.models.CategoryAnnotationOnly;
@@ -144,13 +145,18 @@ public class IciqlSuite {
return Math.abs(expected - actual) <= 0.000001d;
}
+ public static Db openNewDb() {
+ return openNewDb(Mode.PROD);
+ }
+
/**
* Open a new Db object. All connections are cached and re-used to eliminate
* embedded database startup costs.
*
+ * @param mode
* @return a fresh Db object
*/
- public static Db openNewDb() {
+ public static Db openNewDb(Mode mode) {
String testUrl = System.getProperty("iciql.url", DEFAULT_TEST_DB.url);
String testUser = System.getProperty("iciql.user", DEFAULT_TEST_DB.username);
String testPassword = System.getProperty("iciql.password", DEFAULT_TEST_DB.password);
@@ -168,7 +174,7 @@ public class IciqlSuite {
dataSources.put(testUrl, dataSource);
connectionFactories.put(testUrl, factory);
}
- db = Db.open(dataSource);
+ db = Db.open(dataSource, mode);
// drop views
db.dropView(ProductView.class);
diff --git a/src/test/java/com/iciql/test/ProductDaoTest.java b/src/test/java/com/iciql/test/ProductDaoTest.java
index 78987fd..e5549c1 100644
--- a/src/test/java/com/iciql/test/ProductDaoTest.java
+++ b/src/test/java/com/iciql/test/ProductDaoTest.java
@@ -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
index 0000000..cecb056
--- /dev/null
+++ b/src/test/java/iciql.properties
@@ -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'