security: ~
fixes: ~
changes: ~
- additions: ~
+ additions:
+ - Support for specifying custom data type adapters in @IQColumn and Define.typeAdapter()
+ - Added com.iciql.SQLDialectPostgreSQL.JsonStringAdapter
+ - Added com.iciql.SQLDialectPostgreSQL.XmlStringAdapter
+ - Added com.iciql.JavaSerializationTypeAdapter to (de)serialize objects into a BLOB column
dependencyChanges: ~
- contributors: ~
+ contributors:
+ - James Moger
}
#
private static final Map<Object, Token> TOKENS;\r
\r
private static final Map<String, Class<? extends SQLDialect>> DIALECTS;\r
- \r
+\r
private final Connection conn;\r
private final Map<Class<?>, TableDefinition<?>> classMap = Collections\r
.synchronizedMap(new HashMap<Class<?>, TableDefinition<?>>());\r
\r
private boolean skipCreate;\r
private boolean autoSavePoint = true;\r
- \r
+\r
static {\r
TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap<Object, Token>());\r
DIALECTS = Collections.synchronizedMap(new HashMap<String, Class<? extends SQLDialect>>());\r
/**\r
* Register a new/custom dialect class. You can use this method to replace\r
* any existing dialect or to add a new one.\r
- * \r
+ *\r
* @param token\r
* the fully qualified name of the connection class or the\r
* expected result of DatabaseMetaData.getDatabaseProductName()\r
throw new IciqlException(e);\r
}\r
}\r
- \r
+\r
public static Db open(String url) {\r
try {\r
Connection conn = JdbcUtils.getConnection(null, url, null, null);\r
throw new IciqlException(e);\r
}\r
}\r
- \r
+\r
public static Db open(String url, String user, char[] password) {\r
try {\r
Connection conn = JdbcUtils.getConnection(null, url, user, password == null ? null : new String(password));\r
/**\r
* Create a new database instance using a data source. This method is fast,\r
* so that you can always call open() / close() on usage.\r
- * \r
+ *\r
* @param ds\r
* the data source\r
* @return the database instance.\r
return new Db(conn);\r
}\r
\r
- \r
- \r
+\r
+\r
/**\r
* Convenience function to avoid import statements in application code.\r
*/\r
* Merge INSERTS if the record does not exist or UPDATES the record if it\r
* does exist. Not all databases support MERGE and the syntax varies with\r
* the database.\r
- * \r
+ *\r
* If the database does not support a MERGE syntax the dialect can try to\r
* simulate a merge by implementing:\r
* <p>\r
* See the Derby dialect for an implementation of this technique.\r
* <p>\r
* If the dialect does not support merge an IciqlException will be thrown.\r
- * \r
+ *\r
* @param t\r
*/\r
public <T> void merge(T t) {\r
int[] columns = def.mapColumns(wildcardSelect, rs);\r
while (rs.next()) {\r
T item = Utils.newObject(modelClass);\r
- def.readRow(item, rs, columns);\r
+ def.readRow(dialect, item, rs, columns);\r
result.add(item);\r
}\r
} catch (SQLException e) {\r
if (def == null) {\r
upgradeDb();\r
def = new TableDefinition<T>(clazz);\r
- def.mapFields();\r
+ def.mapFields(this);\r
classMap.put(clazz, def);\r
if (Iciql.class.isAssignableFrom(clazz)) {\r
T t = instance(clazz);\r
}\r
return def;\r
}\r
- \r
+\r
<T> boolean hasCreated(Class<T> clazz) {\r
return upgradeChecked.contains(clazz);\r
}\r
throw IciqlException.fromSQL(sql, e);\r
}\r
}\r
- \r
+\r
Savepoint prepareSavepoint() {\r
// don't change auto-commit mode.\r
// don't create save point.\r
conn.setAutoCommit(false);\r
savepoint = conn.setSavepoint();\r
} catch (SQLFeatureNotSupportedException e) {\r
- // jdbc driver does not support save points \r
+ // jdbc driver does not support save points\r
} catch (SQLException e) {\r
throw new IciqlException(e, "Could not create save point");\r
}\r
return savepoint;\r
}\r
- \r
+\r
void commit(Savepoint savepoint) {\r
if (savepoint != null) {\r
try {\r
}\r
}\r
}\r
- \r
+\r
void rollback(Savepoint savepoint) {\r
if (savepoint != null) {\r
try {\r
\r
/**\r
* Run a SQL query directly against the database.\r
- * \r
+ *\r
* Be sure to close the ResultSet with\r
- * \r
+ *\r
* <pre>\r
* JdbcUtils.closeSilently(rs, true);\r
* </pre>\r
- * \r
+ *\r
* @param sql\r
* the SQL statement\r
* @param args\r
\r
/**\r
* Run a SQL query directly against the database.\r
- * \r
+ *\r
* Be sure to close the ResultSet with\r
- * \r
+ *\r
* <pre>\r
* JdbcUtils.closeSilently(rs, true);\r
* </pre>\r
- * \r
+ *\r
* @param sql\r
* the SQL statement\r
* @param args\r
/**\r
* Run a SQL query directly against the database and map the results to the\r
* model class.\r
- * \r
+ *\r
* @param modelClass\r
* the model class to bind the query ResultSet rows into.\r
* @param sql\r
/**\r
* Run a SQL query directly against the database and map the results to the\r
* model class.\r
- * \r
+ *\r
* @param modelClass\r
* the model class to bind the query ResultSet rows into.\r
* @param sql\r
\r
/**\r
* Run a SQL statement directly against the database.\r
- * \r
+ *\r
* @param sql\r
* the SQL statement\r
* @return the update count\r
stat = conn.createStatement();\r
updateCount = stat.executeUpdate(sql);\r
} else {\r
- PreparedStatement ps = conn.prepareStatement(sql); \r
+ PreparedStatement ps = conn.prepareStatement(sql);\r
int i = 1;\r
for (Object arg : args) {\r
ps.setObject(i++, arg);\r
}\r
updateCount = ps.executeUpdate();\r
stat = ps;\r
- } \r
+ }\r
return updateCount;\r
} catch (SQLException e) {\r
throw new IciqlException(e);\r
public boolean getSkipCreate() {\r
return this.skipCreate;\r
}\r
- \r
+\r
/**\r
* Allow to enable/disable usage of save point.\r
* For advanced user wanting to gain full control of transactions.\r
public boolean getAutoSavePoint() {\r
return this.autoSavePoint;\r
}\r
- \r
+\r
}\r
\r
package com.iciql;\r
\r
+import com.iciql.Iciql.DataTypeAdapter;\r
import com.iciql.Iciql.IndexType;\r
\r
/**\r
checkInDefine();\r
currentTableDefinition.defineConstraintUnique(name, columns);\r
}\r
- \r
+\r
/*\r
* The variable argument type Object can't be used twice :-)\r
*/\r
// checkInDefine();\r
// currentTableDefinition.defineForeignKey(name, columns, refTableName, Columns, deleteType, updateType, deferrabilityType);\r
// }\r
- \r
+\r
public static void primaryKey(Object... columns) {\r
checkInDefine();\r
currentTableDefinition.definePrimaryKey(columns);\r
}\r
- \r
+\r
public static void schemaName(String schemaName) {\r
checkInDefine();\r
currentTableDefinition.defineSchemaName(schemaName);\r
checkInDefine();\r
currentTableDefinition.defineViewTableName(viewTableName);\r
}\r
- \r
+\r
public static void memoryTable() {\r
checkInDefine();\r
currentTableDefinition.defineMemoryTable();\r
checkInDefine();\r
currentTableDefinition.defineScale(column, scale);\r
}\r
- \r
+\r
public static void trim(Object column) {\r
checkInDefine();\r
currentTableDefinition.defineTrim(column);\r
}\r
- \r
+\r
public static void nullable(Object column, boolean isNullable) {\r
checkInDefine();\r
currentTableDefinition.defineNullable(column, isNullable);\r
}\r
- \r
+\r
public static void defaultValue(Object column, String defaultValue) {\r
checkInDefine();\r
currentTableDefinition.defineDefaultValue(column, defaultValue);\r
currentTableDefinition.defineConstraint(column, constraint);\r
}\r
\r
+ public static void typeAdapter(Object column, Class<? extends DataTypeAdapter<?>> typeAdapter) {\r
+ checkInDefine();\r
+ currentTableDefinition.defineTypeAdapter(column, typeAdapter);\r
+ }\r
+\r
static synchronized <T> void define(TableDefinition<T> tableDefinition, Iciql table) {\r
currentTableDefinition = tableDefinition;\r
currentTable = table;\r
*/\r
String defaultValue() default "";\r
\r
+ /**\r
+ * Allows specifying a custom data type adapter.\r
+ * <p>\r
+ * For example, you might use this option to map a Postgres JSON column.\r
+ * </p>\r
+ */\r
+ Class<? extends DataTypeAdapter<?>> typeAdapter() default StandardJDBCTypeAdapter.class;\r
+\r
}\r
\r
/**\r
* and the table name.\r
*/\r
void defineIQ();\r
+\r
+ /**\r
+ * Interface to allow implementations of custom data type adapters for supporting\r
+ * database-specific data types, like the Postgres 'json' or 'xml' types,\r
+ * or for supporting other object serialization schemes.\r
+ * <p><b>NOTE:</b> Data type adapters are not thread-safe!</p>\r
+ *\r
+ * @param <T>\r
+ */\r
+ public interface DataTypeAdapter<T> {\r
+\r
+ /**\r
+ * The SQL data type for this adapter.\r
+ *\r
+ * @return the SQL data type\r
+ */\r
+ String getDataType();\r
+\r
+ /**\r
+ * The Java domain type for this adapter.\r
+ *\r
+ * @return the Java domain type\r
+ */\r
+ Class<T> getJavaType();\r
+\r
+ /**\r
+ * Serializes your Java object into a JDBC object.\r
+ *\r
+ * @param value\r
+ * @return a JDBC object\r
+ */\r
+ Object serialize(T value);\r
+\r
+ /**\r
+ * Deserializes a JDBC object into your Java object.\r
+ *\r
+ * @param value\r
+ * @return the Java object\r
+ */\r
+ T deserialize(Object value);\r
+\r
+ }\r
+\r
+ /**\r
+ * The standard type adapter allows iciql to use it's internal utility convert functions\r
+ * to handle the standard JDBC types.\r
+ */\r
+ public final static class StandardJDBCTypeAdapter implements DataTypeAdapter<Object> {\r
+\r
+ @Override\r
+ public String getDataType() {\r
+ throw new RuntimeException("This adapter is for all standard JDBC types.");\r
+ }\r
+\r
+ @Override\r
+ public Class<Object> getJavaType() {\r
+ throw new RuntimeException("This adapter is for all standard JDBC types.");\r
+ }\r
+\r
+ @Override\r
+ public Object serialize(Object value) {\r
+ return value;\r
+ }\r
+\r
+ @Override\r
+ public Object deserialize(Object value) {\r
+ return value;\r
+ }\r
+\r
+ }\r
}\r
--- /dev/null
+/*
+ * 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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.sql.Blob;
+import java.sql.SQLException;
+
+import com.iciql.Iciql.DataTypeAdapter;
+
+/**
+ * Base class for inserting/retrieving a Java Object as a BLOB field using Java Serialization.
+ * <p>You use this by creating a subclass which defines your object class.</p>
+ * <pre>
+ * public class CustomObjectAdapter extends JavaSerializationTypeAdapter<CustomObject> {
+ *
+ * public Class<CustomObject> getJavaType() {
+ * return CustomObject.class;
+ * }
+ * }
+ * </pre>
+ * @param <T>
+ */
+public abstract class JavaSerializationTypeAdapter<T> implements DataTypeAdapter<T> {
+
+ @Override
+ public final String getDataType() {
+ return "BLOB";
+ }
+
+ @Override
+ public final Object serialize(T value) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ new ObjectOutputStream(os).writeObject(value);
+ return os.toByteArray();
+ } catch (IOException e) {
+ throw new IciqlException(e);
+ } finally {
+ try {
+ os.close();
+ } catch (IOException e) {
+ throw new IciqlException (e);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public final T deserialize(Object value) {
+ InputStream is = null;
+ if (value instanceof Blob) {
+ Blob blob = (Blob) value;
+ try {
+ is = blob.getBinaryStream();
+ } catch (SQLException e) {
+ throw new IciqlException(e);
+ }
+ } else if (value instanceof byte[]) {
+ byte [] bytes = (byte []) value;
+ is = new ByteArrayInputStream(bytes);
+ }
+
+ try {
+ T object = (T) new ObjectInputStream(is).readObject();
+ return object;
+ } catch (Exception e) {
+ throw new IciqlException(e);
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ throw new IciqlException (e);
+ }
+ }
+ }
+ }
+}
import java.util.IdentityHashMap;\r
import java.util.List;\r
\r
+import com.iciql.Iciql.EnumType;\r
import com.iciql.NestedConditions.And;\r
import com.iciql.NestedConditions.Or;\r
-import com.iciql.Iciql.EnumType;\r
import com.iciql.bytecode.ClassReader;\r
import com.iciql.util.IciqlLogger;\r
import com.iciql.util.JdbcUtils;\r
int[] columns = def.mapColumns(false, rs);\r
while (rs.next()) {\r
T item = from.newObject();\r
- def.readRow(item, rs, columns);\r
+ def.readRow(db.getDialect(), item, rs, columns);\r
result.add(item);\r
}\r
} catch (SQLException e) {\r
int[] columns = def.mapColumns(false, rs);\r
while (rs.next()) {\r
X row = Utils.newObject(clazz);\r
- def.readRow(row, rs, columns);\r
+ def.readRow(db.getDialect(), row, rs, columns);\r
result.add(row);\r
}\r
} catch (SQLException e) {\r
}\r
\r
private void addParameter(SQLStatement stat, Object alias, Object value) {\r
- if (alias != null && value.getClass().isEnum()) {\r
- SelectColumn<T> col = getColumnByReference(alias);\r
+ SelectColumn<T> col = getColumnByReference(alias);\r
+ if (col != null && value.getClass().isEnum()) {\r
+ // enum\r
EnumType type = col.getFieldDefinition().enumType;\r
Enum<?> anEnum = (Enum<?>) value;\r
Object y = Utils.convertEnum(anEnum, type);\r
stat.addParameter(y);\r
+ } else if (col != null) {\r
+ // object\r
+ Object parameter = db.getDialect().serialize(value, col.getFieldDefinition().typeAdapter);\r
+ stat.addParameter(parameter);\r
} else {\r
+ // primitive\r
stat.addParameter(value);\r
}\r
}\r
import java.sql.DatabaseMetaData;
+import com.iciql.Iciql.DataTypeAdapter;
import com.iciql.TableDefinition.ConstraintForeignKeyDefinition;
import com.iciql.TableDefinition.ConstraintUniqueDefinition;
import com.iciql.TableDefinition.IndexDefinition;
*/
public interface SQLDialect {
+ /**
+ * Returns the registered instance of the type adapter.
+ *
+ * @param typeAdapter
+ * @return the type adapter instance
+ */
+ DataTypeAdapter<?> getTypeAdapter(Class<? extends DataTypeAdapter<?>> typeAdapter);
+
+ /**
+ * Serialize the Java object into a type or format that the database will accept.
+ *
+ * @param value
+ * @param typeAdapter
+ * @return the serialized object
+ */
+ <T> Object serialize(T value, Class<? extends DataTypeAdapter<?>> typeAdapter);
+
+ /**
+ * Deserialize the object received from the database into a Java type.
+ *
+ * @param value
+ * @param typeAdapter
+ * @return the deserialized object
+ */
+ Object deserialize(Object value, Class<? extends DataTypeAdapter<?>> typeAdapter);
+
/**
* Configure the dialect from the database metadata.
- *
+ *
* @param databaseName
* @param data
*/
/**
* Allows a dialect to substitute an SQL type.
- *
+ *
* @param sqlType
* @return the dialect-safe type
*/
/**
* Returns a properly formatted table name for the dialect.
- *
+ *
* @param schemaName
* the schema name, or null for no schema
* @param tableName
/**
* Returns a properly formatted column name for the dialect.
- *
+ *
* @param name
* the column name
* @return the properly formatted column name
/**
* Get the CREATE TABLE statement.
- *
+ *
* @param stat
* @param def
*/
/**
* Get the DROP TABLE statement.
- *
+ *
* @param stat
* @param def
*/
<T> void prepareDropTable(SQLStatement stat, TableDefinition<T> def);
-
+
/**
* Get the CREATE VIEW statement.
- *
+ *
* @param stat
* return the SQL statement
* @param def
/**
* Get the CREATE VIEW statement.
- *
+ *
* @param stat
* return the SQL statement
* @param def
/**
* Get the DROP VIEW statement.
- *
+ *
* @param stat
* return the SQL statement
* @param def
* table definition
*/
<T> void prepareDropView(SQLStatement stat, TableDefinition<T> def);
-
+
/**
* Get the CREATE INDEX statement.
- *
+ *
* @param stat
* return the SQL statement
* @param schemaName
/**
* Get the ALTER statement.
- *
+ *
* @param stat
* return the SQL statement
* @param schemaName
/**
* Get the ALTER statement.
- *
+ *
* @param stat
* return the SQL statement
* @param schemaName
/**
* Get a MERGE or REPLACE INTO statement.
- *
+ *
* @param stat
* return the SQL statement
* @param schemaName
/**
* Append "LIMIT limit OFFSET offset" to the SQL statement.
- *
+ *
* @param stat
* the statement
* @param limit
* Returns the preferred DATETIME class for the database.
* <p>
* Either java.util.Date or java.sql.Timestamp
- *
+ *
* @return preferred DATETIME class
*/
Class<? extends java.util.Date> getDateTimeClass();
/**
* When building static string statements this method flattens an object to
* a string representation suitable for a static string statement.
- *
+ *
* @param o
* @return the string equivalent of this object
*/
- String prepareParameter(Object o);
+ String prepareStringParameter(Object o);
+
}
import java.sql.SQLException;\r
import java.text.MessageFormat;\r
import java.text.SimpleDateFormat;\r
+import java.util.Map;\r
+import java.util.concurrent.ConcurrentHashMap;\r
\r
import com.iciql.Iciql.ConstraintDeleteType;\r
import com.iciql.Iciql.ConstraintUpdateType;\r
+import com.iciql.Iciql.DataTypeAdapter;\r
import com.iciql.TableDefinition.ConstraintForeignKeyDefinition;\r
import com.iciql.TableDefinition.ConstraintUniqueDefinition;\r
import com.iciql.TableDefinition.FieldDefinition;\r
import com.iciql.util.IciqlLogger;\r
import com.iciql.util.StatementBuilder;\r
import com.iciql.util.StringUtils;\r
+import com.iciql.util.Utils;\r
\r
/**\r
* Default implementation of an SQL dialect.\r
int databaseMinorVersion;\r
String databaseName;\r
String productVersion;\r
+ Map<Class<? extends DataTypeAdapter<?>>, DataTypeAdapter<?>> typeAdapters;\r
+\r
+ public SQLDialectDefault() {\r
+ typeAdapters = new ConcurrentHashMap<Class<? extends DataTypeAdapter<?>>, DataTypeAdapter<?>>();\r
+ }\r
\r
@Override\r
public String toString() {\r
buff.appendExceptFirst(", ");\r
buff.append('?');\r
Object value = def.getValue(obj, field);\r
- stat.addParameter(value);\r
+ Object parameter = serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
buff.append(" FROM ");\r
buff.append(prepareTableName(schemaName, tableName));\r
buff.appendExceptFirst(" AND ");\r
buff.append(MessageFormat.format("{0} = ?", prepareColumnName(field.columnName)));\r
Object value = def.getValue(obj, field);\r
- stat.addParameter(value);\r
+ Object parameter = serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
}\r
buff.append(" HAVING count(*)=0)");\r
}\r
\r
@Override\r
- public String prepareParameter(Object o) {\r
+ public DataTypeAdapter<?> getTypeAdapter(Class<? extends DataTypeAdapter<?>> typeAdapter) {\r
+ DataTypeAdapter<?> dtt = typeAdapters.get(typeAdapter);\r
+ if (dtt == null) {\r
+ dtt = Utils.newObject(typeAdapter);\r
+ typeAdapters.put(typeAdapter, dtt);\r
+ }\r
+ return dtt;\r
+ }\r
+\r
+ @SuppressWarnings("unchecked")\r
+ @Override\r
+ public <T> Object serialize(T value, Class<? extends DataTypeAdapter<?>> typeAdapter) {\r
+ if (typeAdapter == null) {\r
+ // pass-through\r
+ return value;\r
+ }\r
+\r
+ DataTypeAdapter<T> dtt = (DataTypeAdapter<T>) getTypeAdapter(typeAdapter);\r
+ return dtt.serialize(value);\r
+ }\r
+\r
+ @Override\r
+ public Object deserialize(Object value, Class<? extends DataTypeAdapter<?>> typeAdapter) {\r
+ DataTypeAdapter<?> dtt = typeAdapters.get(typeAdapter);\r
+ if (dtt == null) {\r
+ dtt = Utils.newObject(typeAdapter);\r
+ typeAdapters.put(typeAdapter, dtt);\r
+ }\r
+\r
+ return dtt.deserialize(value);\r
+ }\r
+\r
+ @Override\r
+ public String prepareStringParameter(Object o) {\r
if (o instanceof String) {\r
return LITERAL + o.toString().replace(LITERAL, "''") + LITERAL;\r
} else if (o instanceof Character) {\r
return "CREATE CACHED TABLE IF NOT EXISTS";\r
}\r
}\r
- \r
+\r
@Override\r
protected <T> String prepareCreateView(TableDefinition<T> def) {\r
return "CREATE VIEW IF NOT EXISTS";\r
buff.appendExceptFirst(", ");\r
buff.append('?');\r
Object value = def.getValue(obj, field);\r
- stat.addParameter(value);\r
+ Object parameter = serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
buff.append(')');\r
stat.setSQL(buff.toString());\r
return "CREATE CACHED TABLE IF NOT EXISTS";\r
}\r
}\r
- \r
+\r
@Override\r
public <T> void prepareDropView(SQLStatement stat, TableDefinition<T> def) {\r
StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS "\r
}\r
buff.append(')');\r
Object value = def.getValue(obj, field);\r
- stat.addParameter(value);\r
+ Object parameter = serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
\r
// map to temporary table\r
protected <T> String prepareCreateTable(TableDefinition<T> def) {\r
return "CREATE TABLE IF NOT EXISTS";\r
}\r
- \r
+\r
@Override\r
public <T> void prepareDropView(SQLStatement stat, TableDefinition<T> def) {\r
StatementBuilder buff = new StatementBuilder("DROP VIEW IF EXISTS "\r
stat.setSQL(buff.toString());\r
return;\r
}\r
- \r
+\r
@Override\r
public String prepareColumnName(String name) {\r
return "`" + name + "`";\r
buff.appendExceptFirst(", ");\r
buff.append('?');\r
Object value = def.getValue(obj, field);\r
- stat.addParameter(value);\r
+ Object parameter = serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
buff.append(") ON DUPLICATE KEY UPDATE ");\r
buff.resetCount();\r
\r
package com.iciql;\r
\r
+import java.sql.SQLException;\r
+\r
+import org.postgresql.util.PGobject;\r
+\r
+import com.iciql.Iciql.DataTypeAdapter;\r
import com.iciql.TableDefinition.IndexDefinition;\r
import com.iciql.util.StatementBuilder;\r
\r
\r
@Override\r
protected boolean prepareColumnDefinition(StatementBuilder buff, String dataType,\r
- boolean isAutoIncrement, boolean isPrimaryKey) { \r
+ boolean isAutoIncrement, boolean isPrimaryKey) {\r
String convertedType = convertSqlType(dataType);\r
if (isIntegerType(dataType)) {\r
if (isAutoIncrement) {\r
}\r
return false;\r
}\r
- \r
+\r
@Override\r
public void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName,\r
IndexDefinition index) {\r
\r
stat.setSQL(buff.toString().trim());\r
}\r
+\r
+ /**\r
+ * Handles transforming raw strings to/from the Postgres JSON data type.\r
+ */\r
+ public class JsonStringAdapter implements DataTypeAdapter<String> {\r
+\r
+ @Override\r
+ public String getDataType() {\r
+ return "json";\r
+ }\r
+\r
+ @Override\r
+ public Class<String> getJavaType() {\r
+ return String.class;\r
+ }\r
+\r
+ @Override\r
+ public Object serialize(String value) {\r
+ PGobject pg = new PGobject();\r
+ pg.setType(getDataType());\r
+ try {\r
+ pg.setValue(value);\r
+ } catch (SQLException e) {\r
+ // not thrown on base PGobject\r
+ }\r
+ return pg;\r
+ }\r
+\r
+ @Override\r
+ public String deserialize(Object value) {\r
+ return value.toString();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Handles transforming raw strings to/from the Postgres XML data type.\r
+ */\r
+ public class XmlStringAdapter implements DataTypeAdapter<String> {\r
+\r
+ @Override\r
+ public String getDataType() {\r
+ return "xml";\r
+ }\r
+\r
+ @Override\r
+ public Class<String> getJavaType() {\r
+ return String.class;\r
+ }\r
+\r
+ @Override\r
+ public Object serialize(String value) {\r
+ PGobject pg = new PGobject();\r
+ pg.setType(getDataType());\r
+ try {\r
+ pg.setValue(value);\r
+ } catch (SQLException e) {\r
+ // not thrown on base PGobject\r
+ }\r
+ return pg;\r
+ }\r
+\r
+ @Override\r
+ public String deserialize(Object value) {\r
+ return value.toString();\r
+ }\r
+ }\r
}
\ No newline at end of file
sb.append('?');\r
} else {\r
// static parameter\r
- sb.append(db.getDialect().prepareParameter(o));\r
+ sb.append(db.getDialect().prepareStringParameter(o));\r
}\r
i++;\r
}\r
import com.iciql.Iciql.ConstraintDeferrabilityType;\r
import com.iciql.Iciql.ConstraintDeleteType;\r
import com.iciql.Iciql.ConstraintUpdateType;\r
+import com.iciql.Iciql.DataTypeAdapter;\r
import com.iciql.Iciql.EnumId;\r
import com.iciql.Iciql.EnumType;\r
import com.iciql.Iciql.IQColumn;\r
import com.iciql.Iciql.IQVersion;\r
import com.iciql.Iciql.IQView;\r
import com.iciql.Iciql.IndexType;\r
+import com.iciql.Iciql.StandardJDBCTypeAdapter;\r
import com.iciql.util.IciqlLogger;\r
import com.iciql.util.StatementBuilder;\r
import com.iciql.util.StringUtils;\r
Class<?> enumTypeClass;\r
boolean isPrimitive;\r
String constraint;\r
+ Class<? extends DataTypeAdapter<?>> typeAdapter;\r
\r
Object getValue(Object obj) {\r
try {\r
\r
private Object initWithNewObject(Object obj) {\r
Object o = Utils.newObject(field.getType());\r
- setValue(obj, o);\r
+ setValue(null, obj, o);\r
return o;\r
}\r
\r
- private void setValue(Object obj, Object o) {\r
+ private void setValue(SQLDialect dialect, Object obj, Object o) {\r
try {\r
if (!field.isAccessible()) {\r
field.setAccessible(true);\r
Class<?> targetType = field.getType();\r
if (targetType.isEnum()) {\r
o = Utils.convertEnum(o, targetType, enumType);\r
+ } else if (dialect != null && typeAdapter != null) {\r
+ o = dialect.deserialize(o, typeAdapter);\r
} else {\r
o = Utils.convert(o, targetType);\r
}\r
}\r
}\r
\r
- void mapFields() {\r
+ void defineTypeAdapter(Object column, Class<? extends DataTypeAdapter<?>> typeAdapter) {\r
+ FieldDefinition def = fieldMap.get(column);\r
+ if (def != null) {\r
+ def.typeAdapter = typeAdapter;\r
+ }\r
+ }\r
+\r
+ void mapFields(Db db) {\r
boolean byAnnotationsOnly = false;\r
boolean inheritColumns = false;\r
if (clazz.isAnnotationPresent(IQTable.class)) {\r
Class<?> enumTypeClass = null;\r
String defaultValue = "";\r
String constraint = "";\r
+ String dataType = null;\r
+ Class<? extends DataTypeAdapter<?>> typeAdapter = null;\r
+\r
// configure Java -> SQL enum mapping\r
if (f.getType().isEnum()) {\r
enumType = EnumType.DEFAULT_TYPE;\r
trim = col.trim();\r
nullable = col.nullable();\r
\r
+ if (col.typeAdapter() != null && col.typeAdapter() != StandardJDBCTypeAdapter.class) {\r
+ typeAdapter = col.typeAdapter();\r
+ DataTypeAdapter<?> dtt = db.getDialect().getTypeAdapter(col.typeAdapter());\r
+ dataType = dtt.getDataType();\r
+ }\r
+\r
// annotation overrides\r
if (!StringUtils.isNullOrEmpty(col.defaultValue())) {\r
defaultValue = col.defaultValue();\r
fieldDef.defaultValue = defaultValue;\r
fieldDef.enumType = enumType;\r
fieldDef.enumTypeClass = enumTypeClass;\r
- fieldDef.dataType = ModelUtils.getDataType(fieldDef);\r
+ fieldDef.dataType = StringUtils.isNullOrEmpty(dataType) ? ModelUtils.getDataType(fieldDef) : dataType;\r
+ fieldDef.typeAdapter = typeAdapter;\r
fieldDef.constraint = constraint;\r
uniqueFields.add(fieldDef);\r
}\r
// try to interpret and instantiate a default value\r
value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass());\r
}\r
- stat.addParameter(value);\r
+ Object parameter = db.getDialect().serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
buff.append(')');\r
stat.setSQL(buff.toString());\r
// try to interpret and instantiate a default value\r
value = ModelUtils.getDefaultValue(field, db.getDialect().getDateTimeClass());\r
}\r
- stat.addParameter(value);\r
+ Object parameter = db.getDialect().serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
buff.append(')');\r
stat.setSQL(buff.toString());\r
buff.appendExceptFirst(", ");\r
buff.append(db.getDialect().prepareColumnName(field.columnName));\r
buff.append(" = ?");\r
- stat.addParameter(value);\r
+ Object parameter = db.getDialect().serialize(value, field.typeAdapter);\r
+ stat.addParameter(parameter);\r
}\r
}\r
Object alias = Utils.newObject(obj.getClass());\r
return columns;\r
}\r
\r
- void readRow(Object item, ResultSet rs, int[] columns) {\r
+ void readRow(SQLDialect dialect, Object item, ResultSet rs, int[] columns) {\r
for (int i = 0; i < fields.size(); i++) {\r
FieldDefinition def = fields.get(i);\r
int index = columns[i];\r
Object o = def.read(rs, index);\r
- def.setValue(item, o);\r
+ def.setValue(dialect, item, o);\r
}\r
}\r
\r
<tr><td>DECIMAL(length,scale)</td><td>can specify length/precision and scale</td><td>--</td></tr>\r
<tr><td>BOOLEAN</td><td>flexible mapping of boolean as bool, varchar, or int</td><td>--</td></tr>\r
<tr><td>BLOB</td><td>partially supported <em>(can not be used in a WHERE clause)</em></td><td>--</td></tr>\r
+<tr><td>Custom</td><td>partially supported <em>uses custom-defined data type adapter (can not be used in a WHERE clause)</em></td><td>--</td></tr>\r
<tr><td>UUID</td><td>fully supported <em>(H2 only)</em> </td><td>--</td></tr>\r
<tr><th colspan="3">configuration</th></tr>\r
<tr><td>DEFAULT values</td><td>set from annotation, <em>default object values</em>, or Define.defaultValue()</td><td>set from annotations</td></tr>\r
can not be directly referenced in an expression</td></tr>\r
<tr><td>byte []</td> <td></td>\r
<td>BLOB</td><tr/>\r
+<tr><td>Custom</td> <td>create a DataTypeAdapter<Custom></td>\r
+<td>Custom</td><tr/>\r
\r
<tr><td colspan="3"><b>H2 Database Types</b><br/>\r
fully supported when paired with an H2 database \r
\r
@IQColumn\r
private Availability availability;\r
+ \r
+ @IQColumn(typeAdapter = MyCustomClassAdapter.class)\r
+ private MyCustomClass;\r
\r
// ignored because it is not annotated AND the class is @IQTable annotated\r
private Integer ignoredField;\r
--- /dev/null
+/*
+ * 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.test;
+
+import java.util.Date;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.iciql.Db;
+import com.iciql.Iciql.IQColumn;
+import com.iciql.Iciql.IQTable;
+import com.iciql.JavaSerializationTypeAdapter;
+import com.iciql.test.models.SupportedTypes;
+
+/**
+ * Tests insertion and retrieval of a custom data type that is automatically transformed
+ * by a Java Object Serialization-based type adapter.
+ */
+public class DataTypeAdapterTest extends Assert {
+
+ private Db db;
+
+
+ @Before
+ public void setUp() {
+ db = IciqlSuite.openNewDb();
+ }
+
+ @After
+ public void tearDown() {
+ db.close();
+ }
+
+ @Test
+ public void testSerializedObjectDataType() {
+
+ SerializedObjectTypeAdapterTest row = new SerializedObjectTypeAdapterTest();
+ row.received = new Date();
+ row.obj = SupportedTypes.createList().get(1);
+ db.insert(row);
+
+ SerializedObjectTypeAdapterTest table = new SerializedObjectTypeAdapterTest();
+ SerializedObjectTypeAdapterTest q1 = db.from(table).selectFirst();
+
+ assertNotNull(q1);
+ assertTrue(row.obj.equivalentTo(q1.obj));
+
+ }
+
+ @IQTable
+ public static class SerializedObjectTypeAdapterTest {
+
+ @IQColumn(autoIncrement = true, primaryKey = true)
+ private long id;
+
+ @IQColumn
+ private java.util.Date received;
+
+ @IQColumn(typeAdapter = SupportedTypesAdapter.class)
+ private SupportedTypes obj;
+
+ }
+
+ /**
+ * Maps a SupportedType instance to a BLOB using Java Object serialization.
+ *
+ */
+ public static class SupportedTypesAdapter extends JavaSerializationTypeAdapter<SupportedTypes> {
+
+ @Override
+ public Class<SupportedTypes> getJavaType() {
+ return SupportedTypes.class;
+ }
+
+ }
+
+}
import com.beust.jcommander.Parameters;\r
import com.iciql.Constants;\r
import com.iciql.Db;\r
+import com.iciql.test.DataTypeAdapterTest.SerializedObjectTypeAdapterTest;\r
import com.iciql.test.models.BooleanModel;\r
import com.iciql.test.models.CategoryAnnotationOnly;\r
import com.iciql.test.models.ComplexObject;\r
@SuiteClasses({ AliasMapTest.class, AnnotationsTest.class, BooleanModelTest.class, ClobTest.class,\r
ConcurrencyTest.class, EnumsTest.class, ModelsTest.class, PrimitivesTest.class, OneOfTest.class,\r
RuntimeQueryTest.class, SamplesTest.class, UpdateTest.class, UpgradesTest.class, JoinTest.class,\r
- UUIDTest.class, ViewsTest.class, ForeignKeyTest.class, TransactionTest.class, NestedConditionsTest.class })\r
+ UUIDTest.class, ViewsTest.class, ForeignKeyTest.class, TransactionTest.class, NestedConditionsTest.class,\r
+ DataTypeAdapterTest.class })\r
public class IciqlSuite {\r
\r
private static final TestDb[] TEST_DBS = {\r
db.dropTable(MultipleBoolsModel.class);\r
db.dropTable(ProductAnnotationOnlyWithForeignKey.class);\r
db.dropTable(CategoryAnnotationOnly.class);\r
+ db.dropTable(SerializedObjectTypeAdapterTest.class);\r
\r
return db;\r
}\r
package com.iciql.test.models;
+import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
@IQTable
@IQIndexes({ @IQIndex({ "myLong", "myInteger" }), @IQIndex(type = IndexType.HASH, value = "myString") })
@IQVersion(1)
-public class SupportedTypes {
+public class SupportedTypes implements Serializable {
+
+ private static final long serialVersionUID = 1L;
public static final SupportedTypes SAMPLE = new SupportedTypes();