diff --git a/build/version.properties b/build/version.properties index a6352b0..f9db8a4 100644 --- a/build/version.properties +++ b/build/version.properties @@ -1,4 +1,4 @@ # $Id$ -build.version=1.1.1 \ No newline at end of file +build.version=1.1.1-DH \ No newline at end of file diff --git a/src/main/net/sf/persist/DefaultNameGuesser.java b/src/main/net/sf/persist/DefaultNameGuesser.java index c16a98c..e55d6d8 100644 --- a/src/main/net/sf/persist/DefaultNameGuesser.java +++ b/src/main/net/sf/persist/DefaultNameGuesser.java @@ -11,22 +11,28 @@ */ public final class DefaultNameGuesser implements NameGuesser { - /** - * Given a field or class name in the form CompoundName (for classes) or - * compoundName (for fields) will return a set of guessed names such as - * [compound_name, compound_names, compoundname, compoundnames]. - */ - public Set guessColumn(final String fieldOrClassName) { + /** + * Given a field or class name in the form CompoundName (for classes) or + * compoundName (for fields) will return a set of guessed names such as + * [compound_name, compound_names, compoundname, compoundnames]. + */ + public Set guessColumn(final String fieldOrClassName) { - final String nameUnderscore = fieldOrClassName.replaceAll("([A-Z])", "_$1").toLowerCase(); - final String nameLowercase = fieldOrClassName.toLowerCase(Locale.ENGLISH); + final String nameUnderscore = fieldOrClassName.replaceAll("([A-Z])", "_$1").toLowerCase(); + final String nameLowercase = fieldOrClassName.toLowerCase(Locale.ENGLISH); - final Set names = new LinkedHashSet(); - names.add(nameUnderscore); - names.add(nameLowercase); - names.add(nameUnderscore + "s"); - names.add(nameLowercase + "s"); - return names; - } + final Set names = new LinkedHashSet(); + names.add(nameUnderscore); + names.add(nameLowercase); + if (fieldOrClassName.endsWith("y")) { + // Handles Countries = Country and Categories = Category etc... + names.add(nameUnderscore.substring(0, nameUnderscore.length() - 1) + "ies"); + names.add(nameLowercase.substring(0, nameLowercase.length() - 1) + "ies"); + } else { + names.add(nameUnderscore + "s"); + names.add(nameLowercase + "s"); + } + return names; + } } diff --git a/src/main/net/sf/persist/Log.java b/src/main/net/sf/persist/Log.java index 46c469d..6112f58 100644 --- a/src/main/net/sf/persist/Log.java +++ b/src/main/net/sf/persist/Log.java @@ -9,161 +9,168 @@ */ public final class Log { - // avoid instantiation - private Log() { - // do nothing - } - - public static final String ENGINE = "persist.engine"; - public static final String PROFILING = "persist.profiling"; - public static final String RESULTS = "persist.results"; - public static final String PARAMETERS = "persist.parameters"; - - private static boolean log4jAvailable = false; - static { - try { - Class.forName("org.apache.log4j.Logger"); - log4jAvailable = true; - } catch (ClassNotFoundException e) { - log4jAvailable = false; - } - } - - public static void trace(final String name, final Object message) { - if (log4jAvailable) { - Logger.getLogger(name).trace(message); - } - } - - public static boolean isTraceEnabled(final String name) { - return log4jAvailable && Logger.getLogger(name).isTraceEnabled(); - } - - public static void debug(final String name, final Object message) { - if (log4jAvailable) { - Logger.getLogger(name).debug(message); - } - } - - public static boolean isDebugEnabled(final String name) { - return log4jAvailable && Logger.getLogger(name).isDebugEnabled(); - } - - public static void info(final String name, final Object message) { - if (log4jAvailable) { - Logger.getLogger(name).info(message); - } - } - - public static boolean isInfoEnabled(final String name) { - return log4jAvailable && Logger.getLogger(name).isInfoEnabled(); - } - - public static void error(final String name, final Object message) { - if (log4jAvailable) { - Logger.getLogger(name).error(message); - } - } - - /** - * Converts types expected in prepared statement parameters to a suitable - * string to be added to log output. - */ - public static String objectToString(final Object obj) { - - String str = null; - - if (obj instanceof String) { - str = (String) obj; - } else if (obj instanceof byte[]) { - str = java.util.Arrays.toString((byte[]) obj); - str = str.substring(1, str.length() - 1); - } else if (obj instanceof Byte[]) { - str = java.util.Arrays.toString((Byte[]) obj); - str = str.substring(1, str.length() - 1); - } else if (obj instanceof char[]) { - str = java.util.Arrays.toString((char[]) obj); - str = str.substring(1, str.length() - 1); - } else if (obj instanceof Character[]) { - str = java.util.Arrays.toString((Character[]) obj); - str = str.substring(1, str.length() - 1); - } else { - str = obj == null ? "null" : obj.toString(); - } - - return (str.length() > 64) ? str.substring(0, 64) + "..." : str; - } - - public static String sqlTypeToString(final int type) { - - final String ret; - - if (type == java.sql.Types.ARRAY) { - ret = "ARRAY"; - } else if (type == java.sql.Types.BIGINT) { - ret = "BIGINT"; - } else if (type == java.sql.Types.BINARY) { - ret = "BINARY"; - } else if (type == java.sql.Types.BIT) { - ret = "BIT"; - } else if (type == java.sql.Types.BLOB) { - ret = "BLOB"; - } else if (type == java.sql.Types.BOOLEAN) { - ret = "BOOLEAN"; - } else if (type == java.sql.Types.CHAR) { - ret = "CHAR"; - } else if (type == java.sql.Types.CLOB) { - ret = "CLOB"; - } else if (type == java.sql.Types.DATALINK) { - ret = "DATALINK"; - } else if (type == java.sql.Types.DATE) { - ret = "DATE"; - } else if (type == java.sql.Types.DECIMAL) { - ret = "DECIMAL"; - } else if (type == java.sql.Types.DISTINCT) { - ret = "DISTINCT"; - } else if (type == java.sql.Types.DOUBLE) { - ret = "DOUBLE"; - } else if (type == java.sql.Types.FLOAT) { - ret = "FLOAT"; - } else if (type == java.sql.Types.INTEGER) { - ret = "INTEGER"; - } else if (type == java.sql.Types.JAVA_OBJECT) { - ret = "JAVA_OBJECT"; - } else if (type == java.sql.Types.LONGVARBINARY) { - ret = "LONGVARBINARY"; - } else if (type == java.sql.Types.LONGVARCHAR) { - ret = "LONGVARCHAR"; - } else if (type == java.sql.Types.NULL) { - ret = "NULL"; - } else if (type == java.sql.Types.NUMERIC) { - ret = "NUMERIC"; - } else if (type == java.sql.Types.OTHER) { - ret = "OTHER"; - } else if (type == java.sql.Types.REAL) { - ret = "REAL"; - } else if (type == java.sql.Types.REF) { - ret = "REF"; - } else if (type == java.sql.Types.SMALLINT) { - ret = "SMALLINT"; - } else if (type == java.sql.Types.STRUCT) { - ret = "STRUCT"; - } else if (type == java.sql.Types.TIME) { - ret = "TIME"; - } else if (type == java.sql.Types.TIMESTAMP) { - ret = "TIMESTAMP"; - } else if (type == java.sql.Types.TINYINT) { - ret = "TINYINT"; - } else if (type == java.sql.Types.VARBINARY) { - ret = "VARBINARY"; - } else if (type == java.sql.Types.VARCHAR) { - ret = "VARCHAR"; - } else { - ret = "" + type; - } - - return ret; - - } + // avoid instantiation + private Log() { + // do nothing + } + + public static final String ENGINE = "persist.engine"; + public static final String PROFILING = "persist.profiling"; + public static final String RESULTS = "persist.results"; + public static final String PARAMETERS = "persist.parameters"; + + private static boolean log4jAvailable = false; + + static { + try { + Class.forName("org.apache.log4j.Logger"); + log4jAvailable = true; + } catch (ClassNotFoundException e) { + log4jAvailable = false; + } + } + + public static void trace(final String name, final Object message) { + if (log4jAvailable) { + Logger.getLogger(name).trace(message); + } + } + + public static boolean isTraceEnabled(final String name) { + return log4jAvailable && Logger.getLogger(name).isTraceEnabled(); + } + + public static void debug(final String name, final Object message) { + if (log4jAvailable) { + Logger.getLogger(name).debug(message); + } + } + + public static boolean isDebugEnabled(final String name) { + return log4jAvailable && Logger.getLogger(name).isDebugEnabled(); + } + + public static void info(final String name, final Object message) { + if (log4jAvailable) { + Logger.getLogger(name).info(message); + } + } + + public static boolean isInfoEnabled(final String name) { + return log4jAvailable && Logger.getLogger(name).isInfoEnabled(); + } + + public static void error(final String name, final Object message) { + if (log4jAvailable) { + Logger.getLogger(name).error(message); + } + } + + public static void warn(final String name, final Object message) { + if (log4jAvailable) { + Logger.getLogger(name).warn(message); + } + } + + /** + * Converts types expected in prepared statement parameters to a suitable + * string to be added to log output. + */ + public static String objectToString(final Object obj) { + + String str = null; + + if (obj instanceof String) { + str = (String) obj; + } else if (obj instanceof byte[]) { + str = java.util.Arrays.toString((byte[]) obj); + str = str.substring(1, str.length() - 1); + } else if (obj instanceof Byte[]) { + str = java.util.Arrays.toString((Byte[]) obj); + str = str.substring(1, str.length() - 1); + } else if (obj instanceof char[]) { + str = java.util.Arrays.toString((char[]) obj); + str = str.substring(1, str.length() - 1); + } else if (obj instanceof Character[]) { + str = java.util.Arrays.toString((Character[]) obj); + str = str.substring(1, str.length() - 1); + } else { + str = obj == null ? "null" : obj.toString(); + } + + return (str.length() > 64) ? str.substring(0, 64) + "..." : str; + } + + public static String sqlTypeToString(final int type) { + + final String ret; + + if (type == java.sql.Types.ARRAY) { + ret = "ARRAY"; + } else if (type == java.sql.Types.BIGINT) { + ret = "BIGINT"; + } else if (type == java.sql.Types.BINARY) { + ret = "BINARY"; + } else if (type == java.sql.Types.BIT) { + ret = "BIT"; + } else if (type == java.sql.Types.BLOB) { + ret = "BLOB"; + } else if (type == java.sql.Types.BOOLEAN) { + ret = "BOOLEAN"; + } else if (type == java.sql.Types.CHAR) { + ret = "CHAR"; + } else if (type == java.sql.Types.CLOB) { + ret = "CLOB"; + } else if (type == java.sql.Types.DATALINK) { + ret = "DATALINK"; + } else if (type == java.sql.Types.DATE) { + ret = "DATE"; + } else if (type == java.sql.Types.DECIMAL) { + ret = "DECIMAL"; + } else if (type == java.sql.Types.DISTINCT) { + ret = "DISTINCT"; + } else if (type == java.sql.Types.DOUBLE) { + ret = "DOUBLE"; + } else if (type == java.sql.Types.FLOAT) { + ret = "FLOAT"; + } else if (type == java.sql.Types.INTEGER) { + ret = "INTEGER"; + } else if (type == java.sql.Types.JAVA_OBJECT) { + ret = "JAVA_OBJECT"; + } else if (type == java.sql.Types.LONGVARBINARY) { + ret = "LONGVARBINARY"; + } else if (type == java.sql.Types.LONGVARCHAR) { + ret = "LONGVARCHAR"; + } else if (type == java.sql.Types.NULL) { + ret = "NULL"; + } else if (type == java.sql.Types.NUMERIC) { + ret = "NUMERIC"; + } else if (type == java.sql.Types.OTHER) { + ret = "OTHER"; + } else if (type == java.sql.Types.REAL) { + ret = "REAL"; + } else if (type == java.sql.Types.REF) { + ret = "REF"; + } else if (type == java.sql.Types.SMALLINT) { + ret = "SMALLINT"; + } else if (type == java.sql.Types.STRUCT) { + ret = "STRUCT"; + } else if (type == java.sql.Types.TIME) { + ret = "TIME"; + } else if (type == java.sql.Types.TIMESTAMP) { + ret = "TIMESTAMP"; + } else if (type == java.sql.Types.TINYINT) { + ret = "TINYINT"; + } else if (type == java.sql.Types.VARBINARY) { + ret = "VARBINARY"; + } else if (type == java.sql.Types.VARCHAR) { + ret = "VARCHAR"; + } else { + ret = "" + type; + } + + return ret; + + } } diff --git a/src/main/net/sf/persist/Mapping.java b/src/main/net/sf/persist/Mapping.java index c62d12c..199d141 100644 --- a/src/main/net/sf/persist/Mapping.java +++ b/src/main/net/sf/persist/Mapping.java @@ -2,6 +2,8 @@ package net.sf.persist; +import net.sf.persist.annotations.Column; + import java.lang.reflect.Method; import java.sql.DatabaseMetaData; import java.sql.SQLException; @@ -10,160 +12,166 @@ public abstract class Mapping { - public abstract Method getGetterForColumn(String columnName); - - public abstract Method getSetterForColumn(String columnName); - - // ---------- utility methods ---------- - - /** - * Factory method to create a Mapping based on a Class. Will return a - * NoTableAnnotation if the class has a NoTable annotation set, or - * TableAnnotation otherwise. - */ - public static final Mapping getMapping(final DatabaseMetaData metaData, final Class objectClass, - final NameGuesser nameGuesser) { - - // get @NoTable annotation - final net.sf.persist.annotations.NoTable noTableAnnotation = (net.sf.persist.annotations.NoTable) objectClass - .getAnnotation(net.sf.persist.annotations.NoTable.class); - - // if @NoTable is set, build a NoTableAnnotation - if (noTableAnnotation != null) { - return new NoTableMapping(objectClass, nameGuesser); - } - - // otherwise, build a TableAnnotation - else { - try { - return new TableMapping(metaData, objectClass, nameGuesser); - } catch (SQLException e) { - throw new PersistException(e); - } - } - } - - /** - * Returns an array with maps for annotations, getters and setters. Keys in - * each map are field names. - */ - protected static final Map[] getFieldsMaps(final Class objectClass) { - final Method[] methods = objectClass.getMethods(); - - // create map with all getters and setters - - final Map allMethods = new HashMap(); - for (Method method : methods) { - final String name = method.getName(); - final String suffix = name.substring(3); - - Method[] getterSetter = allMethods.get(suffix); - if (getterSetter == null) { - getterSetter = new Method[2]; - allMethods.put(suffix, getterSetter); - } - - if (name.startsWith("get")) { - getterSetter[0] = method; - } else if (name.startsWith("set")) { - getterSetter[1] = method; - } - } - - // assemble annotations, getters and setters maps - // a field is only taken into consideration if it has a getter and a - // setter - - final Map annotationsMap = new HashMap(); - final Map gettersMap = new HashMap(); - final Map settersMap = new HashMap(); - - for (String suffix : allMethods.keySet()) { - - final Method[] getterSetter = allMethods.get(suffix); - - // only consider fields to have both getters and setters - if (getterSetter[0] != null && getterSetter[1] != null) { - - // field name (prefix with first char in lower case) - final String fieldName = suffix.substring(0, 1).toLowerCase() + suffix.substring(1); - - // column annotation - final net.sf.persist.annotations.Column getterAnnotation = getterSetter[0] - .getAnnotation(net.sf.persist.annotations.Column.class); - final net.sf.persist.annotations.Column setterAnnotation = getterSetter[1] - .getAnnotation(net.sf.persist.annotations.Column.class); - - // if NoColumn is specified, don't use the field - final net.sf.persist.annotations.NoColumn noPersistGetter = getterSetter[0] - .getAnnotation(net.sf.persist.annotations.NoColumn.class); - - final net.sf.persist.annotations.NoColumn noPersistSetter = getterSetter[1] - .getAnnotation(net.sf.persist.annotations.NoColumn.class); - - // check conflicting NoColumn and Column annotations - if (noPersistGetter != null || noPersistSetter != null) { - if (getterAnnotation != null || setterAnnotation != null) { - throw new PersistException("Field [" + fieldName + "] from class [" + objectClass.getName() - + "] has conflicting NoColumn and Column annotations"); - } - continue; - } - - // assert that getters and setters have valid and compatible - // types - if (getterSetter[1].getParameterTypes().length != 1) { - throw new PersistException("Setter [" + getterSetter[1] - + "] should have a single parameter but has " + getterSetter[1].getParameterTypes().length); - } - if (getterSetter[0].getReturnType() == void.class) { - throw new PersistException("Getter [" + getterSetter[0] + "] must have a return parameter"); - } - if (getterSetter[0].getReturnType() != getterSetter[1].getParameterTypes()[0]) { - throw new PersistException("Getter [" + getterSetter[0] + "] and setter [" + getterSetter[1] - + "] have incompatible types"); - } - - // check for annotations on the getter/setter - net.sf.persist.annotations.Column annotation = null; - - if (getterAnnotation != null && setterAnnotation != null) { - - // if both getter and setter have annotations, make sure - // they are equals - if (!getterAnnotation.equals(setterAnnotation)) { - - final String getterAnn = getterAnnotation.toString().substring( - getterAnnotation.toString().indexOf('(') + 1, - getterAnnotation.toString().lastIndexOf(')')); - - final String setterAnn = setterAnnotation.toString().substring( - setterAnnotation.toString().indexOf('(') + 1, - setterAnnotation.toString().lastIndexOf(')')); - - throw new PersistException("Annotations for getter [" + getterSetter[0] + "] and setter [" - + getterSetter[1] + "] have different annotations [" + getterAnn + "] [" + setterAnn - + "]"); - } - - annotation = getterAnnotation; - } else if (getterAnnotation != null) { - annotation = getterAnnotation; - } else if (setterAnnotation != null) { - annotation = setterAnnotation; - } - - // make getter and setter accessible - getterSetter[0].setAccessible(true); - getterSetter[1].setAccessible(true); - - annotationsMap.put(fieldName, annotation); - gettersMap.put(fieldName, getterSetter[0]); - settersMap.put(fieldName, getterSetter[1]); - } - } - - return new Map[] { annotationsMap, gettersMap, settersMap }; - } + public abstract Method getGetterForColumn(String columnName); + + public abstract Method getSetterForColumn(String columnName); + + // ---------- utility methods ---------- + + /** + * Factory method to create a Mapping based on a Class. Will return a + * NoTableAnnotation if the class has a NoTable annotation set, or + * TableAnnotation otherwise. + */ + public static final Mapping getMapping(final DatabaseMetaData metaData, final Class objectClass, + final NameGuesser nameGuesser) { + + // get @NoTable annotation + final net.sf.persist.annotations.NoTable noTableAnnotation = (net.sf.persist.annotations.NoTable) objectClass + .getAnnotation(net.sf.persist.annotations.NoTable.class); + + // if @NoTable is set, build a NoTableAnnotation + if (noTableAnnotation != null) { + return new NoTableMapping(objectClass, nameGuesser); + } + + // otherwise, build a TableAnnotation + else { + try { + return new TableMapping(metaData, objectClass, nameGuesser); + } catch (SQLException e) { + throw new PersistException(e); + } + } + } + + /** + * Returns an array with maps for annotations, getters and setters. Keys in + * each map are field names. + * @param objectClass class used to match table field names + */ + protected static final Map[] getFieldsMaps(final Class objectClass) { + final Method[] methods = objectClass.getMethods(); + + // create map with all getters and setters + + final Map allMethods = new HashMap(32); + for (Method method : methods) { + final String name = method.getName(); + final String suffix; + if (name.startsWith("is")) { + suffix = name.substring(2); + } else { + suffix = name.substring(3); + } + + Method[] getterSetter = allMethods.get(suffix); + if (getterSetter == null) { + getterSetter = new Method[2]; + allMethods.put(suffix, getterSetter); + } + + if (name.startsWith("get") || name.startsWith("is")) { + getterSetter[0] = method; + } else if (name.startsWith("set")) { + getterSetter[1] = method; + } + } + + // assemble annotations, getters and setters maps + // a field is only taken into consideration if it has a getter and a + // setter + + final Map annotationsMap = new HashMap(32); + final Map gettersMap = new HashMap(32); + final Map settersMap = new HashMap(32); + + for (String suffix : allMethods.keySet()) { + + final Method[] getterSetter = allMethods.get(suffix); + + // only consider fields to have both getters and setters + if (getterSetter[0] != null && getterSetter[1] != null) { + + // field name (prefix with first char in lower case) + final String fieldName = suffix.substring(0, 1).toLowerCase() + suffix.substring(1); + + // column annotation + final Column getterAnnotation = getterSetter[0] + .getAnnotation(Column.class); + final Column setterAnnotation = getterSetter[1] + .getAnnotation(Column.class); + + // if NoColumn is specified, don't use the field + final net.sf.persist.annotations.NoColumn noPersistGetter = getterSetter[0] + .getAnnotation(net.sf.persist.annotations.NoColumn.class); + + final net.sf.persist.annotations.NoColumn noPersistSetter = getterSetter[1] + .getAnnotation(net.sf.persist.annotations.NoColumn.class); + + // check conflicting NoColumn and Column annotations + if (noPersistGetter != null || noPersistSetter != null) { + if (getterAnnotation != null || setterAnnotation != null) { + throw new PersistException("Field [" + fieldName + "] from class [" + objectClass.getName() + + "] has conflicting NoColumn and Column annotations"); + } + continue; + } + + // assert that getters and setters have valid and compatible + // types + if (getterSetter[1].getParameterTypes().length != 1) { + throw new PersistException("Setter [" + getterSetter[1] + + "] should have a single parameter but has " + getterSetter[1].getParameterTypes().length); + } + if (getterSetter[0].getReturnType() == void.class) { + throw new PersistException("Getter [" + getterSetter[0] + "] must have a return parameter"); + } + if (getterSetter[0].getReturnType() != getterSetter[1].getParameterTypes()[0]) { + throw new PersistException("Getter [" + getterSetter[0] + "] and setter [" + getterSetter[1] + + "] have incompatible types"); + } + + // check for annotations on the getter/setter + Column annotation = null; + + if (getterAnnotation != null && setterAnnotation != null) { + + // if both getter and setter have annotations, make sure + // they are equals + if (!getterAnnotation.equals(setterAnnotation)) { + + final String getterAnn = getterAnnotation.toString().substring( + getterAnnotation.toString().indexOf('(') + 1, + getterAnnotation.toString().lastIndexOf(')')); + + final String setterAnn = setterAnnotation.toString().substring( + setterAnnotation.toString().indexOf('(') + 1, + setterAnnotation.toString().lastIndexOf(')')); + + throw new PersistException("Annotations for getter [" + getterSetter[0] + "] and setter [" + + getterSetter[1] + "] have different annotations [" + getterAnn + "] [" + setterAnn + + "]"); + } + + annotation = getterAnnotation; + } else if (getterAnnotation != null) { + annotation = getterAnnotation; + } else if (setterAnnotation != null) { + annotation = setterAnnotation; + } + + // make getter and setter accessible + getterSetter[0].setAccessible(true); + getterSetter[1].setAccessible(true); + + annotationsMap.put(fieldName, annotation); + gettersMap.put(fieldName, getterSetter[0]); + settersMap.put(fieldName, getterSetter[1]); + } + } + + return new Map[]{annotationsMap, gettersMap, settersMap}; + } } diff --git a/src/main/net/sf/persist/NoTableMapping.java b/src/main/net/sf/persist/NoTableMapping.java index 3fa04a7..da310e5 100644 --- a/src/main/net/sf/persist/NoTableMapping.java +++ b/src/main/net/sf/persist/NoTableMapping.java @@ -9,7 +9,7 @@ /** * Represents the mapping of columns to getters and setters of a POJO. - *

+ *

* It is used when a class specifies a * {@link net.sf.persist.annotations.NoTable NoTable} annotation, which means * the class is not mapped to a table in the database, and will be only used to @@ -17,154 +17,152 @@ */ public class NoTableMapping extends Mapping { - // POJO class - private final Class objectClass; - - // map field names to setters - private final Map settersMap; - - // map field names to getters - private final Map gettersMap; - - // map possible column names to field names - private final Map columnsMap; - - public NoTableMapping(Class objectClass, NameGuesser nameGuesser) { - - checkAnnotation(objectClass); - - this.objectClass = objectClass; - - // get the list of annotations, getters and setters - Map[] fieldsMaps = Mapping.getFieldsMaps(objectClass); - final Map annotationsMap = fieldsMaps[0]; - gettersMap = fieldsMaps[1]; - settersMap = fieldsMaps[2]; - - // create columns map by iterating through all fields in the object - // class - // if a field has a @Column annotation, use it, otherwise add all - // guessed names for the field in the map - columnsMap = new HashMap(); - for (String fieldName : gettersMap.keySet()) { - - net.sf.persist.annotations.Column annotation = annotationsMap.get(fieldName); - - // autoGenerated is not supported on @NoTable mappings - if (annotation != null) { - if (annotation.autoGenerated() == true) { - throw new PersistException("@Column(autoGenerated=true) is set for field [" + fieldName - + "] of class [" + objectClass.getCanonicalName() - + " which has been declared with @NoTable"); - } - } - - // if there's a column name specified in the annotation, use it - if (annotation != null && annotation.name() != null) { - - // check if the column name is blank - if (annotation.name().trim().equals("")) { - throw new PersistException("@Column annotation for field [" + fieldName + "] of class [" - + objectClass.getCanonicalName() + "] defines a blank column name"); - } - - // check for name conflicts - checkNameConflicts(fieldName, annotation.name()); - - // add to map - columnsMap.put(annotation.name(), fieldName); - } - - else { // no annotation, add all guessed column names for the field - - Set guessedColumns = nameGuesser.guessColumn(fieldName); - for (String guessedColumn : guessedColumns) { - - // check for name conflicts - checkNameConflicts(fieldName, guessedColumn); - - // add to map - columnsMap.put(guessedColumn, fieldName); - } - } - } - - } - - /** - * Returns the field name associated with a given column. If a mapping can't - * be found, will throw a PersistException. - */ - public String getFieldNameForColumn(String columnName) { - String fieldName = columnsMap.get(columnName); - if (fieldName == null) { - throw new PersistException("Could map field for column [" + columnName + "] on class [" - + objectClass.getCanonicalName() - + "]. Please specify an explict @Column annotation for that column."); - } - return fieldName; - } - - /** - * Returns the setter method associated with a given column. If a mapping - * can't be found, will throw a PersistException. - * - * @see Mapping - */ - public Method getSetterForColumn(String columnName) { - String fieldName = getFieldNameForColumn(columnName); - return settersMap.get(fieldName); - } - - /** - * Returns the getter method associated with a given column. If a mapping - * can't be found, will throw a PersistException. - * - * @see Mapping - */ - public Method getGetterForColumn(String columnName) { - String fieldName = getFieldNameForColumn(columnName); - return gettersMap.get(fieldName); - } - - /** - * Checks if a given column name conflicts with an existing name in the - * columns map. - */ - private void checkNameConflicts(String fieldName, String column) { - String existingFieldName = columnsMap.get(column); - if (existingFieldName != null) { - throw new PersistException("Fields [" + fieldName + "] and [" + existingFieldName - + "] have conflicting column name [" + column - + "] either from guessed names or anotations. " - + "Please specify @Column mappings for at least one of those fields"); - } - } - - /** - * Checks if {@link net.sf.persist.annotations.NoTable NoTable} is present - * and if a conflicting {@link net.sf.persist.annotations.Table Table} is - * not present. - */ - private void checkAnnotation(Class objectClass) { - // get @NoTable annotation - final net.sf.persist.annotations.NoTable noTableAnnotation = (net.sf.persist.annotations.NoTable) objectClass - .getAnnotation(net.sf.persist.annotations.NoTable.class); - - // check if annotation is set - if (noTableAnnotation == null) { - throw new PersistException("Class [" + objectClass.getCanonicalName() - + "] does not specify a @NoTable annotation therefore it can't be mapped through NoTableMapping"); - } - - // check for conflicting @Table annotation - final net.sf.persist.annotations.Table tableAnnotation = (net.sf.persist.annotations.Table) objectClass - .getAnnotation(net.sf.persist.annotations.Table.class); - - if (tableAnnotation != null) { - throw new PersistException("Class [" + objectClass.getCanonicalName() - + "] specifies conflicting @Table and @NoTable annotations"); - } - } + // POJO class + private final Class objectClass; + + // map field names to setters + private final Map settersMap; + + // map field names to getters + private final Map gettersMap; + + // map possible column names to field names + private final Map columnsMap; + + public NoTableMapping(Class objectClass, NameGuesser nameGuesser) { + + checkAnnotation(objectClass); + + this.objectClass = objectClass; + + // get the list of annotations, getters and setters + Map[] fieldsMaps = Mapping.getFieldsMaps(objectClass); + final Map annotationsMap = fieldsMaps[0]; + gettersMap = fieldsMaps[1]; + settersMap = fieldsMaps[2]; + + // create columns map by iterating through all fields in the object + // class + // if a field has a @Column annotation, use it, otherwise add all + // guessed names for the field in the map + columnsMap = new HashMap(); + for (String fieldName : gettersMap.keySet()) { + + net.sf.persist.annotations.Column annotation = annotationsMap.get(fieldName); + + // autoGenerated is not supported on @NoTable mappings + if (annotation != null) { + if (annotation.autoGenerated() == true) { + throw new PersistException("@Column(autoGenerated=true) is set for field [" + fieldName + + "] of class [" + objectClass.getCanonicalName() + + " which has been declared with @NoTable"); + } + } + + // if there's a column name specified in the annotation, use it + if (annotation != null && annotation.name() != null) { + + // check if the column name is blank + if (annotation.name().trim().equals("")) { + throw new PersistException("@Column annotation for field [" + fieldName + "] of class [" + + objectClass.getCanonicalName() + "] defines a blank column name"); + } + + // check for name conflicts + checkNameConflicts(fieldName, annotation.name()); + + // add to map + columnsMap.put(annotation.name(), fieldName); + } else { // no annotation, add all guessed column names for the field + + Set guessedColumns = nameGuesser.guessColumn(fieldName); + for (String guessedColumn : guessedColumns) { + + // check for name conflicts + checkNameConflicts(fieldName, guessedColumn); + + // add to map + columnsMap.put(guessedColumn, fieldName); + } + } + } + + } + + /** + * Returns the field name associated with a given column. If a mapping can't + * be found, will throw a PersistException. + */ + public String getFieldNameForColumn(String columnName) { + String fieldName = columnsMap.get(columnName); + if (fieldName == null) { + throw new PersistException("Could map field for column [" + columnName + "] on class [" + + objectClass.getCanonicalName() + + "]. Please specify an explict @Column annotation for that column."); + } + return fieldName; + } + + /** + * Returns the setter method associated with a given column. If a mapping + * can't be found, will throw a PersistException. + * + * @see Mapping + */ + public Method getSetterForColumn(String columnName) { + String fieldName = getFieldNameForColumn(columnName); + return settersMap.get(fieldName); + } + + /** + * Returns the getter method associated with a given column. If a mapping + * can't be found, will throw a PersistException. + * + * @see Mapping + */ + public Method getGetterForColumn(String columnName) { + String fieldName = getFieldNameForColumn(columnName); + return gettersMap.get(fieldName); + } + + /** + * Checks if a given column name conflicts with an existing name in the + * columns map. + */ + private void checkNameConflicts(String fieldName, String column) { + String existingFieldName = columnsMap.get(column); + if (existingFieldName != null) { + throw new PersistException("Fields [" + fieldName + "] and [" + existingFieldName + + "] have conflicting column name [" + column + + "] either from guessed names or anotations. " + + "Please specify @Column mappings for at least one of those fields"); + } + } + + /** + * Checks if {@link net.sf.persist.annotations.NoTable NoTable} is present + * and if a conflicting {@link net.sf.persist.annotations.Table Table} is + * not present. + */ + private void checkAnnotation(Class objectClass) { + // get @NoTable annotation + final net.sf.persist.annotations.NoTable noTableAnnotation = (net.sf.persist.annotations.NoTable) objectClass + .getAnnotation(net.sf.persist.annotations.NoTable.class); + + // check if annotation is set + if (noTableAnnotation == null) { + throw new PersistException("Class [" + objectClass.getCanonicalName() + + "] does not specify a @NoTable annotation therefore it can't be mapped through NoTableMapping"); + } + + // check for conflicting @Table annotation + final net.sf.persist.annotations.Table tableAnnotation = (net.sf.persist.annotations.Table) objectClass + .getAnnotation(net.sf.persist.annotations.Table.class); + + if (tableAnnotation != null) { + throw new PersistException("Class [" + objectClass.getCanonicalName() + + "] specifies conflicting @Table and @NoTable annotations"); + } + } } diff --git a/src/main/net/sf/persist/Persist.java b/src/main/net/sf/persist/Persist.java index 4e89151..4c92693 100644 --- a/src/main/net/sf/persist/Persist.java +++ b/src/main/net/sf/persist/Persist.java @@ -5,35 +5,23 @@ import java.io.IOException; import java.lang.reflect.Method; import java.math.BigDecimal; -import java.sql.Blob; -import java.sql.Clob; -import java.sql.Connection; -import java.sql.ParameterMetaData; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.sql.*; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * The main class for the persistence engine. - *

+ *

* A Persist instance is bound to a {@link java.sql.Connection} object. * Internally, Persist caches table-object mappings under cache names * that allow for different mappings (most likely from different database * schemas) to coexist. The default cache name is used if no cache name is * specified in the constructor. - *

+ *

* Persist instances are not thread safe, in particular because * {@link java.sql.Connection} objects are not thread safe. - *

+ *

* Persist instances are created with the following defaults: *

    *
  • closePreparedStatementsAfterRead=true This will work for most reads @@ -52,1876 +40,1964 @@ * guessed names such as [compound_name, compound_names, compoundname, * compoundnames]. *
- * + * * @see TableMapping */ public final class Persist { - private Connection connection; - private boolean updateAutoGeneratedKeys = false; - private PreparedStatement lastPreparedStatement = null; - private boolean closePreparedStatementsAfterRead = true; - - private static ConcurrentMap> mappingCaches = new ConcurrentHashMap(); - private static ConcurrentMap nameGuessers = new ConcurrentHashMap(); - - private static final String DEFAULT_CACHE = "default cache"; - - private String cacheName = DEFAULT_CACHE; - private NameGuesser nameGuesser = null; - - static { - mappingCaches.put(DEFAULT_CACHE, new ConcurrentHashMap()); - nameGuessers.put(DEFAULT_CACHE, new DefaultNameGuesser()); - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Caches initialized"); - } - } - - // ---------- constructors ---------- - - /** - * Creates a Persist instance that will use the default cache for - * table-object mappings. - * - * @param connection {@link java.sql.Connection} object to be used - * @since 1.0 - */ - public Persist(Connection connection) { - this(DEFAULT_CACHE, connection); - } - - /** - * Creates a Persist instance that will use the given cache name for - * table-object mappings. - * - * @param cacheName Name of the cache to be used - * @param connection {@link java.sql.Connection} object to be used - * @since 1.0 - */ - public Persist(String cacheName, Connection connection) { - - if (cacheName == null) { - cacheName = DEFAULT_CACHE; - } - - this.cacheName = cacheName; - this.connection = connection; - - this.nameGuesser = nameGuessers.get(cacheName); - if (this.nameGuesser == null) { - // this block may execute more than once from different threads -- - // not a problem, though - this.nameGuesser = new DefaultNameGuesser(); - nameGuessers.put(cacheName, this.nameGuesser); - } - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "New instance for cache [" + cacheName + "] and connection [" + connection + "]"); - } - } - - // ---------- name guesser ---------- - - /** - * Sets the {@link NameGuesser} for a given mappings cache. - * - * @param cacheName Name of the cache to be used - * @param nameGuesser {@link NameGuesser} implementation - * @since 1.0 - */ - public static void setNameGuesser(final String cacheName, final NameGuesser nameGuesser) { - nameGuessers.put(cacheName, nameGuesser); - - // purge mappings cache so that name mappings are coherent - mappingCaches.put(cacheName, new ConcurrentHashMap()); - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Name guesser set for cache [" + cacheName + "]"); - } - } - - /** - * Sets the name guesser for the default mappings cache. - * - * @param nameGuesser {@link NameGuesser} implementation - * @since 1.0 - */ - public static void setNameGuesser(final NameGuesser nameGuesser) { - nameGuessers.put(DEFAULT_CACHE, nameGuesser); - } - - // ---------- autoUpdateGeneratedKeys getter/setter ---------- - - /** - * Sets the behavior for updating auto-generated keys. - * - * @param updateAutoGeneratedKeys if set to true, auto-generated keys will - * be updated after the execution of insert or executeUpdate operations that - * may trigger auto-generation of keys in the database - * @since 1.0 - */ - public void setUpdateAutoGeneratedKeys(final boolean updateAutoGeneratedKeys) { - this.updateAutoGeneratedKeys = updateAutoGeneratedKeys; - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "setUpdateAutoGeneratedKeys(" + updateAutoGeneratedKeys + ")"); - } - } - - /** - * Returns true if updating auto-generated keys is enabled. - */ - public boolean isUpdateAutoGeneratedKeys() { - return updateAutoGeneratedKeys; - } - - // ---------- mappings cache ---------- - - /** - * Returns the mapping for the given object class. - * - * @param objectClass {@link java.lang.Class} object to get a - * {@link TableMapping} for - * @since 1.0 - */ - public Mapping getMapping(final Class objectClass) { - - if (cacheName == null) { - cacheName = DEFAULT_CACHE; - } - - if (!mappingCaches.containsKey(cacheName)) { - // more than one map may end up being inserted here for the same - // cacheName, but this is not problematic - mappingCaches.put(cacheName, new ConcurrentHashMap()); - } - - final ConcurrentMap mappingCache = mappingCaches.get(cacheName); - - if (!mappingCache.containsKey(objectClass)) { - try { - // more than one map may end up being inserted here for the same - // objectClass, but this is not - // problematic - mappingCache.put(objectClass, Mapping.getMapping(connection.getMetaData(), objectClass, nameGuesser)); - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Cached mapping for [" + objectClass.getCanonicalName() + "]"); - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - return mappingCache.get(objectClass); - } - - /** - * Utility method that will get a TableMapping for a given class. If the - * mapping for the class is not a TableMapping, will throw an exception - * specifying the given calling method name. - */ - private TableMapping getTableMapping(Class objectClass, String callingMethodName) { - final Mapping mapping = getMapping(objectClass); - if (!(mapping instanceof TableMapping)) { - throw new PersistException("Class [" + objectClass.getCanonicalName() - + "] has a @NoTable annotation defined, therefore " + callingMethodName + " can't work with it. " - + "If this class is supposed to be mapped to a table, @NoTable should not be used."); - } - return (TableMapping) mapping; - } - - // ---------- connection ---------- - - /** - * Returns the {@link java.sql.Connection Connection} associated with this - * Persist instance. - * @since 1.0 - */ - public Connection getConnection() { - return connection; - } - - /** - * Commits the {@link java.sql.Connection Connection} associated with this - * Persist instance. - * - * @see java.sql.Connection#commit() - * @since 1.0 - */ - public void commit() { - try { - connection.commit(); - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Connection commited"); - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Rolls back the {@link java.sql.Connection Connection} associated with - * this Persist instance. - * - * @see java.sql.Connection#rollback() - * @since 1.0 - */ - public void rollback() { - try { - connection.rollback(); - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Connection rolled back"); - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Sets the auto commit behavior for the - * {@link java.sql.Connection Connection} associated with this Persist - * instance. - * @see java.sql.Connection#setAutoCommit(boolean) - * @since 1.0 - */ - public void setAutoCommit(final boolean autoCommit) { - try { - connection.setAutoCommit(autoCommit); - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Connection setAutoCommit(" + autoCommit + ")"); - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - // ---------- prepared statement ---------- - - /** - * Creates a {@link java.sql.PreparedStatement}, setting the names of the - * auto-generated keys to be retrieved. - * - * @param sql SQL statement to create the {@link java.sql.PreparedStatement} - * from - * @param autoGeneratedKeys names of the columns that will have - * auto-generated values produced during the execution of the - * {@link java.sql.PreparedStatement} - * @since 1.0 - */ - public PreparedStatement getPreparedStatement(final String sql, final String[] autoGeneratedKeys) { - try { - if (autoGeneratedKeys == null || autoGeneratedKeys.length == 0) { - lastPreparedStatement = getPreparedStatement(sql); - } else { - lastPreparedStatement = connection.prepareStatement(sql, autoGeneratedKeys); - } - } catch (SQLException e) { - throw new RuntimeSQLException("Error creating prepared statement for sql [" + sql - + "] with autoGeneratedKeys " + Arrays.toString(autoGeneratedKeys) + ": " + e.getMessage(), e); - } - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Generated PreparedStatement [" + lastPreparedStatement + "] for [" + sql - + "] using autoGeneratedKeys " + Arrays.toString(autoGeneratedKeys)); - } - - return lastPreparedStatement; - } - - /** - * Creates a {@link java.sql.PreparedStatement} with no parameters. - * - * @param sql SQL statement to create the {@link java.sql.PreparedStatement} - * from - * @since 1.0 - */ - public PreparedStatement getPreparedStatement(final String sql) { - - try { - lastPreparedStatement = connection.prepareStatement(sql); - } catch (SQLException e) { - throw new RuntimeSQLException("Error creating prepared statement for sql [" + sql + "]: " + e.getMessage(), - e); - } - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Generated PreparedStatement [" + lastPreparedStatement + "] for [" + sql + "]"); - } - - return lastPreparedStatement; - } - - /** - * Closes a {@link java.sql.PreparedStatement}. - * - * @param statement {@link java.sql.PreparedStatement} to be closed - * @see java.sql.PreparedStatement#close() - * @since 1.0 - */ - public void closePreparedStatement(final PreparedStatement statement) { - try { - if (statement != null) { - statement.close(); - } - } catch (SQLException e) { - throw new RuntimeSQLException("Error closing prepared statement: " + e.getMessage(), e); - } - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "Closed PreparedStatement [" + statement + "]"); - } - } - - /** - * Returns the last {@link java.sql.PreparedStatement} used by the engine. - * - * @since 1.0 - */ - public PreparedStatement getLastPreparedStatement() { - return lastPreparedStatement; - } - - /** - * Closes the last {@link java.sql.PreparedStatement} used by the engine. - * - * @see java.sql.PreparedStatement#close() - * @since 1.0 - */ - public void closeLastPreparedStatement() { - closePreparedStatement(lastPreparedStatement); - lastPreparedStatement = null; - } - - /** - * Sets the behavior for closing {@link java.sql.PreparedStatement} - * instances after execution. This will only affect reads, since any update - * operations (insert, delete, update) will always have their - * {@link java.sql.PreparedStatement} instances automatically closed. - *

- * If a query returns InputStream, Reader, Blob or Clob objects, this should - * be set to false, and closing the PreparedStatement must be controlled - * manually. This is because those datatypes stream data from database after - * the PreparedStatement execution. - * - * @param closePreparedStatementsAfterRead if true, - * {@link java.sql.PreparedStatement} instances for read queries will be - * automatically closed - * @since 1.0 - */ - public void setClosePreparedStatementsAfterRead(final boolean closePreparedStatementsAfterRead) { - this.closePreparedStatementsAfterRead = closePreparedStatementsAfterRead; - - if (Log.isDebugEnabled(Log.ENGINE)) { - Log.debug(Log.ENGINE, "setClosePreparedStatementsAfterRead(" + closePreparedStatementsAfterRead + ")"); - } - } - - /** - * Returns true if {@link java.sql.PreparedStatement} instances are - * automatically closed after read (select or otherwise) queries. - * - * @since 1.0 - */ - public boolean isClosePreparedStatementsAfterRead() { - return this.closePreparedStatementsAfterRead; - } - - // ---------- mappers ---------- - - /** - * Sets parameters in the given prepared statement. - *

- * Parameters will be set using PreparedStatement set methods related with - * the Java types of the parameters, according with the following table: - *

    - *
  • Boolean/boolean: setBoolean - *
  • Byte/byte: setByte - *
  • Short/short: setShort - *
  • Integer/integer: setInt - *
  • Long/long: setLong - *
  • Float/float: setFloat - *
  • Double/double: setDouble - *
  • Character/char: setString - *
  • Character[]/char[]: setString - *
  • Byte[]/byte[]: setBytes - *
  • String: setString - *
  • java.math.BigDecimal: setBigDecimal - *
  • java.io.Reader: setCharacterStream - *
  • java.io.InputStream: setBinaryStream - *
  • java.util.Date: setTimestamp - *
  • java.sql.Date: setDate - *
  • java.sql.Time: setTime - *
  • java.sql.Timestamp: setTimestamp - *
  • java.sql.Clob : setClob - *
  • java.sql.Blob: setBlob - *
- * - * @param stmt {@link java.sql.PreparedStatement} to have parameters set - * into - * @param parameters varargs or Object[] with parameters values - * @throws RuntimeSQLException if a database access error occurs or this - * method is called on a closed PreparedStatement; if a parameter type does - * not have a matching set method (as outlined above) - * @throws RuntimeIOException if an error occurs while reading data from a - * Reader or InputStream parameter - * @since 1.0 - */ - public static void setParameters(final PreparedStatement stmt, final Object[] parameters) { - - // if no parameters, do nothing - if (parameters == null || parameters.length == 0) { - return; - } - - ParameterMetaData stmtMetaData = null; - - for (int i = 1; i <= parameters.length; i++) { - - final Object parameter = parameters[i - 1]; - - if (parameter == null) { - - // lazy assignment of stmtMetaData - if (stmtMetaData == null) { - try { - stmtMetaData = stmt.getParameterMetaData(); - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - // get sql type from prepared statement metadata - int sqlType; - try { - sqlType = stmtMetaData.getParameterType(i); - } catch (SQLException e2) { - // feature not supported, use NULL - sqlType = java.sql.Types.NULL; - } - - try { - stmt.setNull(i, sqlType); - } catch (SQLException e) { - throw new RuntimeSQLException("Could not set null into parameter [" + i - + "] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "] " + e.getMessage(), e); - } - - if (Log.isDebugEnabled(Log.PARAMETERS)) { - Log.debug(Log.PARAMETERS, "Parameter [" + i + "] from PreparedStatement [" + stmt - + "] set to [null] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "]"); - } - - continue; - } - - try { - - final Class type = parameter.getClass(); - - if (type == Boolean.class || type == boolean.class) { - stmt.setBoolean(i, (Boolean) parameter); - } else if (type == Byte.class || type == byte.class) { - stmt.setByte(i, (Byte) parameter); - } else if (type == Short.class || type == short.class) { - stmt.setShort(i, (Short) parameter); - } else if (type == Integer.class || type == int.class) { - stmt.setInt(i, (Integer) parameter); - } else if (type == Long.class || type == long.class) { - stmt.setLong(i, (Long) parameter); - } else if (type == Float.class || type == float.class) { - stmt.setFloat(i, (Float) parameter); - } else if (type == Double.class || type == double.class) { - stmt.setDouble(i, (Double) parameter); - } else if (type == Character.class || type == char.class) { - stmt.setString(i, parameter == null ? null : "" + (Character) parameter); - } else if (type == char[].class) { - // not efficient, will create a new String object - stmt.setString(i, parameter == null ? null : new String((char[]) parameter)); - } else if (type == Character[].class) { - // not efficient, will duplicate the array and create a new String object - final Character[] src = (Character[]) parameter; - final char[] dst = new char[src.length]; - for (int j = 0; j < src.length; j++) { // can't use System.arraycopy here - dst[j] = src[j]; - } - stmt.setString(i, new String(dst)); - } else if (type == String.class) { - stmt.setString(i, (String) parameter); - } else if (type == BigDecimal.class) { - stmt.setBigDecimal(i, (BigDecimal) parameter); - } else if (type == byte[].class) { - stmt.setBytes(i, (byte[]) parameter); - } else if (type == Byte[].class) { - // not efficient, will duplicate the array - final Byte[] src = (Byte[]) parameter; - final byte[] dst = new byte[src.length]; - for (int j = 0; j < src.length; j++) { // can't use System.arraycopy here - dst[j] = src[j]; - } - stmt.setBytes(i, dst); - } else if (parameter instanceof java.io.Reader) { - final java.io.Reader reader = (java.io.Reader) parameter; - - // the jdbc api for setCharacterStream requires the number - // of characters to be read so this will end up reading - // data twice (here and inside the jdbc driver) - // besides, the reader must support reset() - int size = 0; - try { - reader.reset(); - while (reader.read() != -1) { - size++; - } - reader.reset(); - } catch (IOException e) { - throw new RuntimeIOException(e); - } - stmt.setCharacterStream(i, reader, size); - } else if (parameter instanceof java.io.InputStream) { - final java.io.InputStream inputStream = (java.io.InputStream) parameter; - - // the jdbc api for setBinaryStream requires the number of - // bytes to be read so this will end up reading the stream - // twice (here and inside the jdbc driver) - // besides, the stream must support reset() - int size = 0; - try { - inputStream.reset(); - while (inputStream.read() != -1) { - size++; - } - inputStream.reset(); - } catch (IOException e) { - throw new RuntimeIOException(e); - } - stmt.setBinaryStream(i, inputStream, size); - } else if (parameter instanceof Clob) { - stmt.setClob(i, (Clob) parameter); - } else if (parameter instanceof Blob) { - stmt.setBlob(i, (Blob) parameter); - } else if (type == java.util.Date.class) { - final java.util.Date date = (java.util.Date) parameter; - stmt.setTimestamp(i, new java.sql.Timestamp(date.getTime())); - } else if (type == java.sql.Date.class) { - stmt.setDate(i, (java.sql.Date) parameter); - } else if (type == java.sql.Time.class) { - stmt.setTime(i, (java.sql.Time) parameter); - } else if (type == java.sql.Timestamp.class) { - stmt.setTimestamp(i, (java.sql.Timestamp) parameter); - } else { - // last resort; this should cover all database-specific - // object types - stmt.setObject(i, parameter); - } - - if (Log.isDebugEnabled(Log.PARAMETERS)) { - Log.debug(Log.PARAMETERS, "PreparedStatement [" + stmt + "] Parameter [" + i + "] type [" - + type.getSimpleName() + "] set to [" + Log.objectToString(parameter) + "]"); - } - - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - } - - /** - * Returns true if the provided class is a type supported natively (as - * opposed to a bean). - * - * @param type {@link java.lang.Class} type to be tested - * @since 1.0 - */ - private static boolean isNativeType(final Class type) { - - // to return an arbitrary object use Object.class - - return (type == boolean.class || type == Boolean.class || type == byte.class || type == Byte.class - || type == short.class || type == Short.class || type == int.class || type == Integer.class - || type == long.class || type == Long.class || type == float.class || type == Float.class - || type == double.class || type == Double.class || type == char.class || type == Character.class - || type == byte[].class || type == Byte[].class || type == char[].class || type == Character[].class - || type == String.class || type == BigDecimal.class || type == java.util.Date.class - || type == java.sql.Date.class || type == java.sql.Time.class || type == java.sql.Timestamp.class - || type == java.io.InputStream.class || type == java.io.Reader.class || type == java.sql.Clob.class - || type == java.sql.Blob.class || type == Object.class); - } - - /** - * Reads a column from the current row in the provided - * {@link java.sql.ResultSet} and returns an instance of the specified Java - * {@link java.lang.Class} containing the values read. - *

- * This method is used while converting {@link java.sql.ResultSet} rows to - * objects. The class type is the field type in the target bean. - *

- * Correspondence between class types and ResultSet.get methods is as - * follows: - *

    - *
  • Boolean/boolean: getBoolean - *
  • Byte/byte: getByte - *
  • Short/short: getShort - *
  • Integer/int: getInt - *
  • Long/long: getLong - *
  • Float/float: getFloat - *
  • Double/double: getDouble - *
  • Character/char: getString - *
  • Character[]/char[]: getString - *
  • Byte[]/byte[]: setBytes - *
  • String: setString - *
  • java.math.BigDecimal: getBigDecimal - *
  • java.io.Reader: getCharacterStream - *
  • java.io.InputStream: getBinaryStream - *
  • java.util.Date: getTimestamp - *
  • java.sql.Date: getDate - *
  • java.sql.Time: getTime - *
  • java.sql.Timestamp: getTimestamp - *
  • java.sql.Clob: getClob - *
  • java.sql.Blob: getBlob - *
- *

- * null's will be respected for any non-native types. This means that if a - * field is of type Integer it will be able to receive a null value from the - * ResultSet; on the other hand, if a field is of type int it will receive 0 - * for a null value from the {@link java.sql.ResultSet}. - * - * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be - * processed) - * @param column column index in the result set (starting with 1) - * @param type {@link java.lang.Class} of the object to be returned - * @since 1.0 - */ - public static Object getValueFromResultSet(final ResultSet resultSet, final int column, final Class type) { - - Object value = null; - - try { - - if (type == boolean.class) { - value = resultSet.getBoolean(column); - } else if (type == Boolean.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column); - } else if (type == byte.class) { - value = resultSet.getByte(column); - } else if (type == Byte.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getByte(column); - } else if (type == short.class) { - value = resultSet.getShort(column); - } else if (type == Short.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getShort(column); - } else if (type == int.class) { - value = resultSet.getInt(column); - } else if (type == Integer.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); - } else if (type == long.class) { - value = resultSet.getLong(column); - } else if (type == Long.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getLong(column); - } else if (type == float.class) { - value = resultSet.getFloat(column); - } else if (type == Float.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column); - } else if (type == double.class) { - value = resultSet.getDouble(column); - } else if (type == Double.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); - } else if (type == BigDecimal.class) { - value = resultSet.getObject(column) == null ? null : resultSet.getBigDecimal(column); - } else if (type == String.class) { - value = resultSet.getString(column); - } else if (type == Character.class || type == char.class) { - final String str = resultSet.getString(column); - if (str != null && str.length() > 1) { - throw new PersistException("Column [" + column + "] returned a string with length [" - + str.length() + "] but field type [" + type.getSimpleName() - + "] can only accept 1 character"); - } - value = (str == null || str.length() == 0) ? null : str.charAt(0); - } else if (type == byte[].class || type == Byte[].class) { - value = resultSet.getBytes(column); - } else if (type == char[].class || type == Character[].class) { - final String str = resultSet.getString(column); - value = (str == null) ? null : str.toCharArray(); - } else if (type == java.util.Date.class) { - final java.sql.Timestamp timestamp = resultSet.getTimestamp(column); - value = (timestamp == null) ? null : new java.util.Date(timestamp.getTime()); - } else if (type == java.sql.Date.class) { - value = resultSet.getDate(column); - } else if (type == java.sql.Time.class) { - value = resultSet.getTime(column); - } else if (type == java.sql.Timestamp.class) { - value = resultSet.getTimestamp(column); - } else if (type == java.io.InputStream.class) { - value = resultSet.getBinaryStream(column); - } else if (type == java.io.Reader.class) { - value = resultSet.getCharacterStream(column); - } else if (type == java.sql.Clob.class) { - value = resultSet.getClob(column); - } else if (type == java.sql.Blob.class) { - value = resultSet.getBlob(column); - } else { - // this will work for database-specific types - value = resultSet.getObject(column); - } - - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - - if (Log.isDebugEnabled(Log.RESULTS)) { - Log.debug(Log.RESULTS, "Read ResultSet [" + resultSet + "] column [" + column + "]" - + (value == null ? "" : " type [" + value.getClass().getSimpleName() + "]") + " value [" - + Log.objectToString(value) + "]"); - } - - return value; - } - - /** - * Reads a column from the current row in the provided - * {@link java.sql.ResultSet} and return a value correspondent to the SQL - * type provided (as defined in {@link java.sql.Types java.sql.Types}). - *

- * This method is used while converting results sets to maps. The SQL type - * comes from the {@link java.sql.ResultSetMetaData ResultSetMetaData} for a - * given column. - *

- * Correspondence between {@link java.sql.Types java.sql.Types} and - * ResultSet.get methods is as follows: - *

    - *
  • ARRAY: getArray - *
  • BIGINT: getLong - *
  • BIT: getBoolean - *
  • BLOB: getBytes - *
  • BOOLEAN: getBoolean - *
  • CHAR: getString - *
  • CLOB: getString - *
  • DATALINK: getBinaryStream - *
  • DATE: getDate - *
  • DECIMAL: getBigDecimal - *
  • DOUBLE: getDouble - *
  • FLOAT: getFloat - *
  • INTEGER: getInt - *
  • JAVA_OBJECT: getObject - *
  • LONGVARBINARY: getBytes - *
  • LONGVARCHAR: getString - *
  • NULL: getNull - *
  • NCHAR: getString - *
  • NUMERIC: getBigDecimal - *
  • OTHER: getObject - *
  • REAL: getDouble - *
  • REF: getRef - *
  • SMALLINT: getInt - *
  • TIME: getTime - *
  • TIMESTAMP: getTimestamp - *
  • TINYINT: getInt - *
  • VARBINARY: getBytes - *
  • VARCHAR: getString - *
  • [Oracle specific] 100: getFloat - *
  • [Oracle specific] 101: getDouble - *
- *

- * null's are respected for all types. This means that if a column is of - * type LONG and its value comes from the database as null, this method will - * return null for it. - * - * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be - * processed) - * @param column Column index in the result set (starting with 1) - * @param type type of the column (as defined in - * {@link java.sql.Types java.sql.Types}) - * @since 1.0 - */ - public static Object getValueFromResultSet(final ResultSet resultSet, final int column, final int type) { - - Object value = null; - - try { - - if (type == java.sql.Types.ARRAY) { - value = resultSet.getArray(column); - } else if (type == java.sql.Types.BIGINT) { - value = resultSet.getObject(column) == null ? null : resultSet.getLong(column); - } else if (type == java.sql.Types.BINARY) { - value = resultSet.getBytes(column); - } else if (type == java.sql.Types.BIT) { - value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column); - } else if (type == java.sql.Types.BLOB) { - value = resultSet.getBytes(column); - } else if (type == java.sql.Types.BOOLEAN) { - value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column); - } else if (type == java.sql.Types.CHAR) { - value = resultSet.getString(column); - } else if (type == java.sql.Types.CLOB) { - value = resultSet.getString(column); - } else if (type == java.sql.Types.DATALINK) { - value = resultSet.getBinaryStream(column); - } else if (type == java.sql.Types.DATE) { - value = resultSet.getDate(column); - } else if (type == java.sql.Types.DECIMAL) { - value = resultSet.getBigDecimal(column); - } else if (type == java.sql.Types.DOUBLE) { - value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); - } else if (type == java.sql.Types.FLOAT) { - value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column); - } else if (type == java.sql.Types.INTEGER) { - value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); - } else if (type == java.sql.Types.JAVA_OBJECT) { - value = resultSet.getObject(column); - } else if (type == java.sql.Types.LONGVARBINARY) { - value = resultSet.getBytes(column); - } else if (type == java.sql.Types.LONGVARCHAR) { - value = resultSet.getString(column); - } else if (type == java.sql.Types.NULL) { - value = null; - } else if (type == java.sql.Types.NUMERIC) { - value = resultSet.getBigDecimal(column); - } else if (type == java.sql.Types.OTHER) { - value = resultSet.getObject(column); - } else if (type == java.sql.Types.REAL) { - value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); - } else if (type == java.sql.Types.REF) { - value = resultSet.getRef(column); - } else if (type == java.sql.Types.SMALLINT) { - value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); - } else if (type == java.sql.Types.TIME) { - value = resultSet.getTime(column); - } else if (type == java.sql.Types.TIMESTAMP) { - value = resultSet.getTimestamp(column); - } else if (type == java.sql.Types.TINYINT) { - value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); - } else if (type == java.sql.Types.VARBINARY) { - value = resultSet.getBytes(column); - } else if (type == java.sql.Types.VARCHAR) { - value = resultSet.getString(column); - } - - // oracle specific - else if (type == 100) { - value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column); - } else if (type == 101) { - value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); - } - - else { - throw new PersistException("Could not get value for result set using type [" - + Log.sqlTypeToString(type) + "] on column [" + column + "]"); - } - - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - - if (Log.isDebugEnabled(Log.RESULTS)) { - Log.debug(Log.RESULTS, "Read ResultSet [" + resultSet + "] column [" + column + "] sql type [" - + Log.sqlTypeToString(type) + "]" - + (value == null ? "" : " type [" + value.getClass().getSimpleName() + "]") + " value [" - + Log.objectToString(value) + "]"); - } - - return value; - } - - /** - * Returns a list of values for the fields in the provided object that match - * the provided list of columns according with the object mapping. - * - * @param object source object to obtain parameter values - * @param columns name of the database columns to get parameters for - * @param mapping mapping for the object class - * @since 1.0 - */ - private static Object[] getParametersFromObject(final Object object, final String[] columns, - final TableMapping mapping) { - - Object[] parameters = new Object[columns.length]; - for (int i = 0; i < columns.length; i++) { - final String columnName = columns[i]; - final Method getter = mapping.getGetterForColumn(columnName); - - Object value = null; - try { - value = getter.invoke(object, new Object[] {}); - } catch (Exception e) { - throw new PersistException("Could not access getter for column [" + columnName + "]", e); - } - - parameters[i] = value; - } - - return parameters; - } - - /** - * Reads a row from the provided {@link java.sql.ResultSet} and converts it - * to an object instance of the given class. - *

- * See {@link #getValueFromResultSet(ResultSet, int, Class)} for details on - * the mappings between ResultSet.get methods and Java types. - * - * @param objectClass type of the object to be returned - * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be - * processed) - * @see #isNativeType(Class) - * @see #getValueFromResultSet(ResultSet, int, Class) - * @since 1.0 - */ - public Object loadObject(final Class objectClass, final ResultSet resultSet) throws SQLException { - - final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); - - Object ret = null; - - // for native objects (int, long, String, Date, etc.) - if (isNativeType(objectClass)) { - if (resultSetMetaData.getColumnCount() != 1) { - throw new PersistException("ResultSet returned [" + resultSetMetaData.getColumnCount() - + "] columns but 1 column was expected to load data into an instance of [" - + objectClass.getName() + "]"); - } - ret = getValueFromResultSet(resultSet, 1, objectClass); - } - - // for beans - else { - - final Mapping mapping = getMapping(objectClass); - - try { - ret = objectClass.newInstance(); - } catch (Exception e) { - throw new PersistException(e); - } - - for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { - final String columnName = resultSetMetaData.getColumnName(i).toLowerCase(); - final Method setter = mapping.getSetterForColumn(columnName); - if (setter == null) { - throw new PersistException("Column [" + columnName - + "] from result set does not have a mapping to a field in [" - + objectClass.getName() + "]"); - } - - final Class type = setter.getParameterTypes()[0]; - final Object value = getValueFromResultSet(resultSet, i, type); - - try { - setter.invoke(ret, new Object[] { value }); - } catch (Exception e) { - throw new PersistException("Error setting value [" + value + "]" - + (value == null ? "" : " of type [" + value.getClass().getName() + "]") + " from column [" - + columnName + "] using setter [" + setter + "]: " + e.getMessage(), e); - } - } - - } - - return ret; - } - - /** - * Reads a row from the provided {@link java.sql.ResultSet} and converts it - * to a map having the column names as keys and results as values. - *

- * See {@link #getValueFromResultSet(ResultSet, int, int)} for details on - * the mappings between ResultSet.get methods and - * {@link java.sql.Types java.sql.Types} types - * - * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be - * processed) - * @since 1.0 - */ - public static Map loadMap(final ResultSet resultSet) throws SQLException { - - final Map ret = new LinkedHashMap(); - final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); - - for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { - final String columnName = resultSetMetaData.getColumnName(i).toLowerCase(); - final int type = resultSetMetaData.getColumnType(i); - final Object value = getValueFromResultSet(resultSet, i, type); - ret.put(columnName, value); - } - - return ret; - } - - /** - * Set auto-generated keys (returned from an insert operation) into an - * object. - * - * @param object {@link java.lang.Object} to have fields mapped to - * auto-generated keys set - * @param result {@link Result} containing auto-generated keys - * @since 1.0 - */ - public void setAutoGeneratedKeys(final Object object, final Result result) { - - if (result.getGeneratedKeys().size() != 0) { - final TableMapping mapping = getTableMapping(object.getClass(), "setAutoGeneratedKeys()"); - for (int i = 0; i < mapping.getAutoGeneratedColumns().length; i++) { - final String columnName = mapping.getAutoGeneratedColumns()[i]; - final Method setter = mapping.getSetterForColumn(columnName); - final Object key = result.getGeneratedKeys().get(i); - try { - setter.invoke(object, new Object[] { key }); - } catch (Exception e) { - throw new PersistException("Could not invoke setter [" + setter + "] with auto generated key [" - + key + "] of class [" + key.getClass().getName() + "]", e); - } - } - } - - } - - // ---------- execute ---------- - - /** - * Executes an update and return a {@link Result} object containing the - * number of rows modified and auto-generated keys produced. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @param objectClass Class of the object related with the query. Used to - * determine the types of the auto-incremented keys. Only important if - * autoGeneratedKeys contains values. - * @param sql SQL code to be executed. - * @param autoGeneratedKeys List of columns that are going to be - * auto-generated during the query execution. - * @param parameters Parameters to be used in the PreparedStatement. - * @since 1.0 - */ - public Result executeUpdate(final Class objectClass, final String sql, final String[] autoGeneratedKeys, - final Object...parameters) { - - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - final PreparedStatement stmt = getPreparedStatement(sql, autoGeneratedKeys); - - try { - setParameters(stmt, parameters); - - int rowsModified = 0; - try { - rowsModified = stmt.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters " - + Arrays.toString(parameters) + ": " + e.getMessage(), e); - } - - final List generatedKeys = new ArrayList(); - if (autoGeneratedKeys.length != 0) { - try { - final Mapping mapping = getMapping(objectClass); - final ResultSet resultSet = stmt.getGeneratedKeys(); - for (int i = 0; i < autoGeneratedKeys.length; i++) { - resultSet.next(); - - // get the auto-generated key using the ResultSet.get method - // that matches - // the bean setter parameter type - final Method setter = mapping.getSetterForColumn(autoGeneratedKeys[i]); - final Class type = setter.getParameterTypes()[0]; - final Object value = Persist.getValueFromResultSet(resultSet, 1, type); - - generatedKeys.add(value); - } - resultSet.close(); - } catch (SQLException e) { - throw new RuntimeSQLException("This JDBC driver does not support PreparedStatement.getGeneratedKeys()." - + " Please use setUpdateAutoGeneratedKeys(false) in your Persist instance" - + " to disable attempts to use that feature"); - } - } - - Result result = new Result(rowsModified, generatedKeys); - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "executeUpdate in [" + (end - begin) + "ms] for sql [" + sql + "]"); - } - - return result; - } finally { - closePreparedStatement(stmt); - } - } - - /** - * Executes an update and returns the number of rows modified. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @param sql SQL code to be executed. - * @param parameters Parameters to be used in the PreparedStatement. - * @since 1.0 - */ - public int executeUpdate(final String sql, final Object...parameters) { - - final PreparedStatement stmt = getPreparedStatement(sql); - int rowsModified = 0; - - try { - setParameters(stmt, parameters); - rowsModified = stmt.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters " - + Arrays.toString(parameters) + ": " + e.getMessage(), e); - } finally { - closePreparedStatement(stmt); - } - - return rowsModified; - } - - // ---------- insert ---------- - - /** - * Inserts an object into the database. - * - * @since 1.0 - */ - public int insert(final Object object) { - final TableMapping mapping = getTableMapping(object.getClass(), "insert()"); - final String sql = mapping.getInsertSql(); - final String[] columns = mapping.getNotAutoGeneratedColumns(); - final Object[] parameters = getParametersFromObject(object, columns, mapping); - - int ret = 0; - if (updateAutoGeneratedKeys) { - if (mapping.supportsGetGeneratedKeys()) { - final Result result = executeUpdate(object.getClass(), sql, mapping.getAutoGeneratedColumns(), - parameters); - setAutoGeneratedKeys(object, result); - ret = result.getRowsModified(); - } else { - throw new PersistException("While inserting instance of [" + object.getClass().getName() - + "] autoUpdateGeneratedKeys is set to [true] but the database doesn't support this feature"); - } - } else { - ret = executeUpdate(sql, parameters); - } - return ret; - } - - /** - * Inserts a batch of objects into the database. - * - * @since 1.0 - */ - // TODO: use batch updates - public int[] insertBatch(final Object...objects) { - if (objects == null || objects.length == 0) { - return new int[0]; - } - final int[] results = new int[objects.length]; - for (int i = 0; i < objects.length; i++) { - results[i] = insert(objects[i]); - } - return results; - } - - // ---------- update ---------- - - /** - * Updates an object in the database. The object will be identified using - * its mapped table's primary key. If no primary keys are defined in the - * mapped table, a {@link PersistException} will be thrown. - * - * @since 1.0 - */ - public int update(final Object object) { - final TableMapping mapping = getTableMapping(object.getClass(), "update()"); - - if (mapping.getPrimaryKeys().length == 0) { - throw new PersistException("Table " + mapping.getTableName() + " doesn't have a primary key"); - } - final String sql = mapping.getUpdateSql(); - final String[] columns = new String[mapping.getNotPrimaryKeys().length + mapping.getPrimaryKeys().length]; - int i = 0; - for (String notPrimaryKey : mapping.getNotPrimaryKeys()) { - columns[i++] = notPrimaryKey; - } - for (String primaryKey : mapping.getPrimaryKeys()) { - columns[i++] = primaryKey; - } - final Object[] parameters = getParametersFromObject(object, columns, mapping); - return executeUpdate(sql, parameters); - } - - /** - * Updates a batch of objects in the database. The objects will be - * identified using their mapped table's primary keys. If no primary keys - * are defined in the mapped table, a {@link PersistException} will be - * thrown. - * - * @since 1.0 - */ - public int[] updateBatch(final Object...objects) { - // TODO: use batch updates - if (objects == null || objects.length == 0) { - return new int[0]; - } - int[] results = new int[objects.length]; - for (int i = 0; i < objects.length; i++) { - results[i] = update(objects[i]); - } - return results; - } - - // ---------- delete ---------- - - /** - * Deletes an object in the database. The object will be identified using - * its mapped table's primary key. If no primary keys are defined in the - * mapped table, a PersistException will be thrown. - * - * @since 1.0 - */ - public int delete(final Object object) { - final TableMapping mapping = getTableMapping(object.getClass(), "delete()"); - if (mapping.getPrimaryKeys().length == 0) { - throw new PersistException("Table " + mapping.getTableName() + " doesn't have a primary key"); - } - final String sql = mapping.getDeleteSql(); - final String[] columns = mapping.getPrimaryKeys(); - final Object[] parameters = getParametersFromObject(object, columns, mapping); - return executeUpdate(sql, parameters); - } - - /** - * Updates a batch of objects in the database. The objects will be - * identified using their matched table's primary keys. If no primary keys - * are defined in a given object, a PersistException will be thrown. - * - * @since 1.0 - */ - public int[] deleteBatch(final Object...objects) { - // TODO: use batch updates - if (objects == null || objects.length == 0) { - return new int[0]; - } - int[] results = new int[objects.length]; - for (int i = 0; i < objects.length; i++) { - results[i] = delete(objects[i]); - } - return results; - } - - // ---------- read ---------- - - // --- single objects --- - - /** - * Reads a single object from the database by mapping the results of the SQL - * query into an instance of the given object class. Only the columns - * returned from the SQL query will be set into the object instance. If a - * given column can't be mapped to the target object instance, a - * {@link PersistException} will be thrown. - * - * @since 1.0 - */ - public T read(final Class objectClass, final String sql) { - return read(objectClass, sql, (Object[]) null); - } - - /** - * Reads a single object from the database by mapping the results of the - * parameterized SQL query into an instance of the given object class. Only - * the columns returned from the SQL query will be set into the object - * instance. If a given column can't be mapped to the target object - * instance, a {@link PersistException} will be thrown. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public T read(final Class objectClass, final String sql, final Object...parameters) { - final PreparedStatement stmt = getPreparedStatement(sql); - return read(objectClass, stmt, parameters); - } - - /** - * Reads a single object from the database by mapping the results of the - * execution of the given PreparedStatement into an instance of the given - * object class. Only the columns returned from the PreparedStatement - * execution will be set into the object instance. If a given column can't - * be mapped to the target object instance, a {@link PersistException} - * will be thrown. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public T read(final Class objectClass, final PreparedStatement statement, final Object...parameters) { - try { - setParameters(statement, parameters); - final ResultSet resultSet = statement.executeQuery(); - final T ret = read(objectClass, resultSet); - return ret; - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } finally { - if (closePreparedStatementsAfterRead) { - closePreparedStatement(statement); - } - } - } - - /** - * Reads a single object from the database by mapping the content of the - * ResultSet current row into an instance of the given object class. Only - * columns contained in the ResultSet will be set into the object instance. - * If a given column can't be mapped to the target object instance, a - * PersistException will be thrown. - * - * @since 1.0 - */ - public T read(final Class objectClass, final ResultSet resultSet) { - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - Object ret = null; - try { - if (resultSet.next()) { - ret = loadObject(objectClass, resultSet); - if (resultSet.next()) { - throw new PersistException("Non-unique result returned"); - } - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "read in [" + (end - begin) + "ms] for object type [" - + objectClass.getSimpleName() + "]"); - } - - return (T) ret; - } - - /** - * Reads an object from the database by its primary keys. - * - * @since 1.0 - */ - public T readByPrimaryKey(final Class objectClass, final Object...primaryKeyValues) { - final TableMapping mapping = getTableMapping(objectClass, "readByPrimaryKey()"); - final String sql = mapping.getSelectSql(); - return read(objectClass, sql, primaryKeyValues); - } - - // --- lists --- - - /** - * Reads a list of objects from the database by mapping the content of the - * ResultSet into instances of the given object class. Only columns - * contained in the ResultSet will be set into the object instances. If a - * given column can't be mapped to a target object instance, a - * PersistException will be thrown. - * - * @since 1.0 - */ - public List readList(final Class objectClass, final ResultSet resultSet) { - - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - final List ret = new ArrayList(); - try { - while (resultSet.next()) { - ret.add((T) loadObject(objectClass, resultSet)); - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "readList in [" + (end - begin) + "ms] for object type [" - + objectClass.getSimpleName() + "]"); - } - - return ret; - } - - /** - * Reads a list of objects from the database by mapping the results of the - * execution of the given PreparedStatement into instances of the given - * object class. Only the columns returned from the PreparedStatement - * execution will be set into the object instances. If a given column can't - * be mapped to a target object instance, a PersistException will be - * thrown. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public List readList(final Class objectClass, final PreparedStatement statement, - final Object...parameters) { - setParameters(statement, parameters); - try { - final ResultSet resultSet = statement.executeQuery(); - return readList(objectClass, resultSet); - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Reads a list of objects from the database by mapping the results of the - * parameterized SQL query into instances of the given object class. Only - * the columns returned from the SQL query will be set into the object - * instance. If a given column can't be mapped to the target object - * instance, a {@link PersistException} will be thrown. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public List readList(final Class objectClass, final String sql, final Object...parameters) { - final PreparedStatement stmt = getPreparedStatement(sql); - try { - return readList(objectClass, stmt, parameters); - } finally { - if (closePreparedStatementsAfterRead) { - closePreparedStatement(stmt); - } - } - } - - /** - * Reads a list of objects from the database by mapping the results of the - * SQL query into instances of the given object class. Only the columns - * returned from the SQL query will be set into the object instance. If a - * given column can't be mapped to the target object instance, a - * {@link PersistException} will be thrown. - * - * @since 1.0 - */ - public List readList(final Class objectClass, final String sql) { - return readList(objectClass, sql, (Object[]) null); - } - - /** - * Reads a list of all objects in the database mapped to the given object - * class. - * - * @since 1.0 - */ - public List readList(final Class objectClass) { - final TableMapping mapping = getTableMapping(objectClass, "readList(Class)"); - final String sql = mapping.getSelectAllSql(); - return readList(objectClass, sql); - } - - // --- iterators --- - - /** - * Returns an {@link java.util.Iterator} for a list of objects from the - * database that map the contents of the ResultSet into instances of the - * given object class. Only columns contained in the ResultSet will be set - * into the object instances. If a given column can't be mapped to a target - * object instance, a {@link PersistException} will be thrown. - * - * @since 1.0 - */ - public Iterator readIterator(final Class objectClass, final ResultSet resultSet) { - - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - final ResultSetIterator i = new ResultSetIterator(this, objectClass, resultSet, ResultSetIterator.TYPE_OBJECT); - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "readIterator in [" + (end - begin) + "ms] for object type [" - + objectClass.getSimpleName() + "]"); - } - - return i; - } - - /** - * Returns an {@link java.util.Iterator} for a list of objects from the - * database that map the contents of the execution of the given - * PreparedStatement into instances of the given object class. Only columns - * contained in the ResultSet will be set into the object instances. If a - * given column can't be mapped to a target object instance, a - * PersistException will be thrown. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public Iterator readIterator(final Class objectClass, final PreparedStatement statement, - final Object...parameters) { - setParameters(statement, parameters); - try { - final ResultSet resultSet = statement.executeQuery(); - return readIterator(objectClass, resultSet); - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Returns an {@link java.util.Iterator} for a list of objects from the - * database that map the contents of the execution of the given SQL query - * into instances of the given object class. Only columns contained in the - * ResultSet will be set into the object instances. If a given column can't - * be mapped to a target object instance, a {@link PersistException} will - * be thrown. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public Iterator readIterator(final Class objectClass, final String sql, final Object...parameters) { - final PreparedStatement stmt = getPreparedStatement(sql); - final Iterator ret = readIterator(objectClass, stmt, parameters); - // don't close the prepared statement otherwise the result set in the - // iterator will be closed - return ret; - } - - /** - * Returns an {@link java.util.Iterator} for a list of objects from the - * database that map the contents of the execution of the given SQL query - * into instances of the given object class. Only columns contained in the - * ResultSet will be set into the object instances. If a given column can't - * be mapped to a target object instance, a {@link PersistException} will - * be thrown. - * - * @since 1.0 - */ - public Iterator readIterator(final Class objectClass, final String sql) { - return readIterator(objectClass, sql, (Object[]) null); - } - - /** - * Returns an {@link java.util.Iterator} for a list of all objects in the - * database mapped to the given object class. - * - * @since 1.0 - */ - public Iterator readIterator(final Class objectClass) { - final TableMapping mapping = getTableMapping(objectClass, "readIterator(Class)"); - final String sql = mapping.getSelectAllSql(); - return readIterator(objectClass, sql); - } - - // ---------- read (map) ---------- - - // --- single objects --- - - /** - * Reads a single object from the database by mapping the results of the SQL - * query into an instance of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - * - * @since 1.0 - */ - public Map readMap(final String sql) { - return readMap(sql, (Object[]) null); - } - - /** - * Reads a single object from the database by mapping the results of the SQL - * query into an instance of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - *

- * Parameters will be set according with the correspondence defined in - * {@link Persist#setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public Map readMap(final String sql, final Object...parameters) { - final PreparedStatement stmt = getPreparedStatement(sql); - try { - return readMap(stmt, parameters); - } finally { - if (closePreparedStatementsAfterRead) { - closePreparedStatement(stmt); - } - } - } - - /** - * Reads a single object from the database by mapping the results of the - * PreparedStatement execution into an instance of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - *

- * Parameters will be set according with the correspondence defined in - * {@link Persist#setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public Map readMap(final PreparedStatement statement, final Object...parameters) { - setParameters(statement, parameters); - try { - final ResultSet resultSet = statement.executeQuery(); - return readMap(resultSet); - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Reads a single object from the database by mapping the results of the - * current ResultSet row into an instance of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - * - * @since 1.0 - */ - public Map readMap(final ResultSet resultSet) { - - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - Map ret = null; - - try { - if (resultSet.next()) { - ret = loadMap(resultSet); - if (resultSet.next()) { - throw new PersistException("Non-unique result returned"); - } - } else { - ret = null; - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "readMap in [" + (end - begin) + "ms]"); - } - - return ret; - } - - // --- list --- - - /** - * Reads a list of objects from the database by mapping the ResultSet rows - * to instances of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - * - * @since 1.0 - */ - public List> readMapList(final ResultSet resultSet) { - - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - final List ret = new ArrayList(); - try { - while (resultSet.next()) { - ret.add(loadMap(resultSet)); - } - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "readMapList [" + (end - begin) + "ms]"); - } - - return ret; - } - - /** - * Reads a list of objects from the database by mapping the - * PreparedStatement execution results to instances of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public List> readMapList(final PreparedStatement statement, final Object...parameters) { - setParameters(statement, parameters); - try { - final ResultSet resultSet = statement.executeQuery(); - return readMapList(resultSet); - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Reads a list of objects from the database by mapping the SQL execution - * results to instances of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public List> readMapList(final String sql, final Object...parameters) { - final PreparedStatement stmt = getPreparedStatement(sql); - try { - return readMapList(stmt, parameters); - } finally { - if (closePreparedStatementsAfterRead) { - closePreparedStatement(stmt); - } - } - } - - /** - * Reads a list of all objects in the database mapped to the given object - * class and return each result as an instance of {@link java.util.Map}. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - * - * @since 1.0 - */ - public List> readMapList(final String sql) { - return readMapList(sql, (Object[]) null); - } - - // --- iterator --- - - /** - * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} - * instances containing data from the provided ResultSet rows. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - * - * @since 1.0 - */ - public Iterator readMapIterator(final ResultSet resultSet) { - return new ResultSetIterator(this, null, resultSet, ResultSetIterator.TYPE_MAP); - } - - /** - * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} - * instances containing data from the execution of the provided - * PreparedStatement. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - *

- * Parameters will be set according with the correspondence defined in - * {@link Persist#setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public Iterator readMapIterator(final PreparedStatement statement, final Object...parameters) { - setParameters(statement, parameters); - try { - final ResultSet resultSet = statement.executeQuery(); - return readMapIterator(resultSet); - } catch (SQLException e) { - throw new RuntimeSQLException(e); - } - } - - /** - * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} - * instances containing data from the execution of the provided parametrized - * SQL. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - *

- * Parameters will be set according with the correspondence defined in - * {@link #setParameters(PreparedStatement, int[], Object[])} - * - * @since 1.0 - */ - public Iterator readMapIterator(final String sql, final Object...parameters) { - - long begin = 0; - if (Log.isDebugEnabled(Log.PROFILING)) { - begin = System.currentTimeMillis(); - } - - final PreparedStatement stmt = getPreparedStatement(sql); - final Iterator ret = readMapIterator(stmt, parameters); - - if (Log.isDebugEnabled(Log.PROFILING)) { - final long end = System.currentTimeMillis(); - Log.debug(Log.PROFILING, "readMapIterator in [" + (end - begin) + "ms]"); - } - - return ret; - } - - /** - * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} - * instances containing data from the execution of the provided SQL. - *

- * Types returned from the database will be converted to Java types in the - * map according with the correspondence defined in - * {@link #getValueFromResultSet(ResultSet, int, int)}. - * - * @since 1.0 - */ - public Iterator readMapIterator(final String sql) { - return readMapIterator(sql, (Object[]) null); - } + private Connection connection; + private boolean updateAutoGeneratedKeys = false; + private PreparedStatement lastPreparedStatement = null; + private boolean closePreparedStatementsAfterRead = true; + + private static ConcurrentMap> mappingCaches = new ConcurrentHashMap(); + private static ConcurrentMap nameGuessers = new ConcurrentHashMap(); + + private static final String DEFAULT_CACHE = "default cache"; + + private String cacheName = DEFAULT_CACHE; + private NameGuesser nameGuesser = null; + + static { + mappingCaches.put(DEFAULT_CACHE, new ConcurrentHashMap()); + nameGuessers.put(DEFAULT_CACHE, new DefaultNameGuesser()); + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Caches initialized"); + } + } + + // ---------- constructors ---------- + + /** + * Creates a Persist instance that will use the default cache for + * table-object mappings. + * + * @param connection {@link java.sql.Connection} object to be used + * @since 1.0 + */ + public Persist(Connection connection) { + this(DEFAULT_CACHE, connection); + } + + /** + * Creates a Persist instance that will use the given cache name for + * table-object mappings. + * + * @param cacheName Name of the cache to be used + * @param connection {@link java.sql.Connection} object to be used + * @since 1.0 + */ + public Persist(String cacheName, Connection connection) { + + if (cacheName == null) { + cacheName = DEFAULT_CACHE; + } + + this.cacheName = cacheName; + this.connection = connection; + + this.nameGuesser = nameGuessers.get(cacheName); + if (this.nameGuesser == null) { + // this block may execute more than once from different threads -- + // not a problem, though + this.nameGuesser = new DefaultNameGuesser(); + nameGuessers.put(cacheName, this.nameGuesser); + } + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "New instance for cache [" + cacheName + "] and connection [" + connection + "]"); + } + } + + // ---------- name guesser ---------- + + /** + * Sets the {@link NameGuesser} for a given mappings cache. + * + * @param cacheName Name of the cache to be used + * @param nameGuesser {@link NameGuesser} implementation + * @since 1.0 + */ + public static void setNameGuesser(final String cacheName, final NameGuesser nameGuesser) { + nameGuessers.put(cacheName, nameGuesser); + + // purge mappings cache so that name mappings are coherent + mappingCaches.put(cacheName, new ConcurrentHashMap()); + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Name guesser set for cache [" + cacheName + "]"); + } + } + + /** + * Sets the name guesser for the default mappings cache. + * + * @param nameGuesser {@link NameGuesser} implementation + * @since 1.0 + */ + public static void setNameGuesser(final NameGuesser nameGuesser) { + nameGuessers.put(DEFAULT_CACHE, nameGuesser); + } + + // ---------- autoUpdateGeneratedKeys getter/setter ---------- + + /** + * Sets the behavior for updating auto-generated keys. + * + * @param updateAutoGeneratedKeys if set to true, auto-generated keys will + * be updated after the execution of insert or executeUpdate operations that + * may trigger auto-generation of keys in the database + * @since 1.0 + */ + public void setUpdateAutoGeneratedKeys(final boolean updateAutoGeneratedKeys) { + this.updateAutoGeneratedKeys = updateAutoGeneratedKeys; + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "setUpdateAutoGeneratedKeys(" + updateAutoGeneratedKeys + ")"); + } + } + + /** + * Returns true if updating auto-generated keys is enabled. + */ + public boolean isUpdateAutoGeneratedKeys() { + return updateAutoGeneratedKeys; + } + + // ---------- mappings cache ---------- + + /** + * Returns the mapping for the given object class. + * + * @param objectClass {@link java.lang.Class} object to get a + * {@link TableMapping} for + * @since 1.0 + */ + public Mapping getMapping(final Class objectClass) { + + if (cacheName == null) { + cacheName = DEFAULT_CACHE; + } + + if (!mappingCaches.containsKey(cacheName)) { + // more than one map may end up being inserted here for the same + // cacheName, but this is not problematic + mappingCaches.put(cacheName, new ConcurrentHashMap()); + } + + final ConcurrentMap mappingCache = mappingCaches.get(cacheName); + + if (!mappingCache.containsKey(objectClass)) { + try { + // more than one map may end up being inserted here for the same + // objectClass, but this is not + // problematic + mappingCache.put(objectClass, Mapping.getMapping(connection.getMetaData(), objectClass, nameGuesser)); + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Cached mapping for [" + objectClass.getCanonicalName() + "]"); + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + return mappingCache.get(objectClass); + } + + /** + * Utility method that will get a TableMapping for a given class. If the + * mapping for the class is not a TableMapping, will throw an exception + * specifying the given calling method name. Uses protected access for Unit Tests. + */ + protected final TableMapping getTableMapping(Class objectClass, String callingMethodName) { + final Mapping mapping = getMapping(objectClass); + if (!(mapping instanceof TableMapping)) { + throw new PersistException("Class [" + objectClass.getCanonicalName() + + "] has a @NoTable annotation defined, therefore " + callingMethodName + " can't work with it. " + + "If this class is supposed to be mapped to a table, @NoTable should not be used."); + } + return (TableMapping) mapping; + } + + // ---------- connection ---------- + + /** + * Returns the {@link java.sql.Connection Connection} associated with this + * Persist instance. + * + * @since 1.0 + */ + public Connection getConnection() { + return connection; + } + + /** + * Commits the {@link java.sql.Connection Connection} associated with this + * Persist instance. + * + * @see java.sql.Connection#commit() + * @since 1.0 + */ + public void commit() { + try { + connection.commit(); + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Connection commited"); + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Rolls back the {@link java.sql.Connection Connection} associated with + * this Persist instance. + * + * @see java.sql.Connection#rollback() + * @since 1.0 + */ + public void rollback() { + try { + connection.rollback(); + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Connection rolled back"); + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Sets the auto commit behavior for the + * {@link java.sql.Connection Connection} associated with this Persist + * instance. + * + * @see java.sql.Connection#setAutoCommit(boolean) + * @since 1.0 + */ + public void setAutoCommit(final boolean autoCommit) { + try { + connection.setAutoCommit(autoCommit); + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Connection setAutoCommit(" + autoCommit + ")"); + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + // ---------- prepared statement ---------- + + /** + * Creates a {@link java.sql.PreparedStatement}, setting the names of the + * auto-generated keys to be retrieved. + * + * @param sql SQL statement to create the {@link java.sql.PreparedStatement} + * from + * @param autoGeneratedKeys names of the columns that will have + * auto-generated values produced during the execution of the + * {@link java.sql.PreparedStatement} + * @since 1.0 + */ + public PreparedStatement getPreparedStatement(final String sql, final String[] autoGeneratedKeys) { + try { + if (autoGeneratedKeys == null || autoGeneratedKeys.length == 0) { + lastPreparedStatement = getPreparedStatement(sql); + } else { + lastPreparedStatement = connection.prepareStatement(sql, autoGeneratedKeys); + } + } catch (SQLException e) { + throw new RuntimeSQLException("Error creating prepared statement for sql [" + sql + + "] with autoGeneratedKeys " + Arrays.toString(autoGeneratedKeys) + ": " + e.getMessage(), e); + } + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Generated PreparedStatement [" + lastPreparedStatement + "] for [" + sql + + "] using autoGeneratedKeys " + Arrays.toString(autoGeneratedKeys)); + } + + return lastPreparedStatement; + } + + /** + * Creates a {@link java.sql.PreparedStatement} with no parameters. + * + * @param sql SQL statement to create the {@link java.sql.PreparedStatement} + * from + * @since 1.0 + */ + public PreparedStatement getPreparedStatement(final String sql) { + + try { + lastPreparedStatement = connection.prepareStatement(sql); + } catch (SQLException e) { + throw new RuntimeSQLException("Error creating prepared statement for sql [" + sql + "]: " + e.getMessage(), + e); + } + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Generated PreparedStatement [" + lastPreparedStatement + "] for [" + sql + "]"); + } + + return lastPreparedStatement; + } + + /** + * Closes a {@link java.sql.PreparedStatement}. + * + * @param statement {@link java.sql.PreparedStatement} to be closed + * @see java.sql.PreparedStatement#close() + * @since 1.0 + */ + public void closePreparedStatement(final PreparedStatement statement) { + try { + if (statement != null) { + statement.close(); + } + } catch (SQLException e) { + throw new RuntimeSQLException("Error closing prepared statement: " + e.getMessage(), e); + } + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "Closed PreparedStatement [" + statement + "]"); + } + } + + /** + * Returns the last {@link java.sql.PreparedStatement} used by the engine. + * + * @since 1.0 + */ + public PreparedStatement getLastPreparedStatement() { + return lastPreparedStatement; + } + + /** + * Closes the last {@link java.sql.PreparedStatement} used by the engine. + * + * @see java.sql.PreparedStatement#close() + * @since 1.0 + */ + public void closeLastPreparedStatement() { + closePreparedStatement(lastPreparedStatement); + lastPreparedStatement = null; + } + + /** + * Sets the behavior for closing {@link java.sql.PreparedStatement} + * instances after execution. This will only affect reads, since any update + * operations (insert, delete, update) will always have their + * {@link java.sql.PreparedStatement} instances automatically closed. + *

+ * If a query returns InputStream, Reader, Blob or Clob objects, this should + * be set to false, and closing the PreparedStatement must be controlled + * manually. This is because those datatypes stream data from database after + * the PreparedStatement execution. + * + * @param closePreparedStatementsAfterRead + * if true, + * {@link java.sql.PreparedStatement} instances for read queries will be + * automatically closed + * @since 1.0 + */ + public void setClosePreparedStatementsAfterRead(final boolean closePreparedStatementsAfterRead) { + this.closePreparedStatementsAfterRead = closePreparedStatementsAfterRead; + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, "setClosePreparedStatementsAfterRead(" + closePreparedStatementsAfterRead + ")"); + } + } + + /** + * Returns true if {@link java.sql.PreparedStatement} instances are + * automatically closed after read (select or otherwise) queries. + * + * @since 1.0 + */ + public boolean isClosePreparedStatementsAfterRead() { + return this.closePreparedStatementsAfterRead; + } + + // ---------- mappers ---------- + + /** + * Sets parameters in the given prepared statement. + *

+ * Parameters will be set using PreparedStatement set methods related with + * the Java types of the parameters, according with the following table: + *

    + *
  • Boolean/boolean: setBoolean + *
  • Byte/byte: setByte + *
  • Short/short: setShort + *
  • Integer/integer: setInt + *
  • Long/long: setLong + *
  • Float/float: setFloat + *
  • Double/double: setDouble + *
  • Character/char: setString + *
  • Character[]/char[]: setString + *
  • Byte[]/byte[]: setBytes + *
  • String: setString + *
  • java.math.BigDecimal: setBigDecimal + *
  • java.io.Reader: setCharacterStream + *
  • java.io.InputStream: setBinaryStream + *
  • java.util.Date: setTimestamp + *
  • java.sql.Date: setDate + *
  • java.sql.Time: setTime + *
  • java.sql.Timestamp: setTimestamp + *
  • java.sql.Clob : setClob + *
  • java.sql.Blob: setBlob + *
+ * + * @param stmt {@link java.sql.PreparedStatement} to have parameters set + * into + * @param parameters varargs or Object[] with parameters values + * @throws RuntimeSQLException if a database access error occurs or this + * method is called on a closed PreparedStatement; if a parameter type does + * not have a matching set method (as outlined above) + * @throws RuntimeIOException if an error occurs while reading data from a + * Reader or InputStream parameter + * @since 1.0 + */ + public static void setParameters(final PreparedStatement stmt, final Object[] parameters) { + + // if no parameters, do nothing + if (parameters == null || parameters.length == 0) { + return; + } + + ParameterMetaData stmtMetaData = null; + + for (int i = 1; i <= parameters.length; i++) { + + final Object parameter = parameters[i - 1]; + + if (parameter == null) { + + // lazy assignment of stmtMetaData + if (stmtMetaData == null) { + try { + stmtMetaData = stmt.getParameterMetaData(); + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + // get sql type from prepared statement metadata + int sqlType; + try { + sqlType = stmtMetaData.getParameterType(i); + } catch (SQLException e2) { + // feature not supported, use NULL + sqlType = java.sql.Types.NULL; + } + + try { + stmt.setNull(i, sqlType); + } catch (SQLException e) { + throw new RuntimeSQLException("Could not set null into parameter [" + i + + "] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "] " + e.getMessage(), e); + } + + if (Log.isDebugEnabled(Log.PARAMETERS)) { + Log.debug(Log.PARAMETERS, "Parameter [" + i + "] from PreparedStatement [" + stmt + + "] set to [null] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "]"); + } + + continue; + } + + try { + + final Class type = parameter.getClass(); + + if (type == Boolean.class || type == boolean.class) { + stmt.setBoolean(i, (Boolean) parameter); + } else if (type == Byte.class || type == byte.class) { + stmt.setByte(i, (Byte) parameter); + } else if (type == Short.class || type == short.class) { + stmt.setShort(i, (Short) parameter); + } else if (type == Integer.class || type == int.class) { + stmt.setInt(i, (Integer) parameter); + } else if (type == Long.class || type == long.class) { + stmt.setLong(i, (Long) parameter); + } else if (type == Float.class || type == float.class) { + stmt.setFloat(i, (Float) parameter); + } else if (type == Double.class || type == double.class) { + stmt.setDouble(i, (Double) parameter); + } else if (type == Character.class || type == char.class) { + stmt.setString(i, parameter == null ? null : "" + (Character) parameter); + } else if (type == char[].class) { + // not efficient, will create a new String object + stmt.setString(i, parameter == null ? null : new String((char[]) parameter)); + } else if (type == Character[].class) { + // not efficient, will duplicate the array and create a new String object + final Character[] src = (Character[]) parameter; + final char[] dst = new char[src.length]; + for (int j = 0; j < src.length; j++) { // can't use System.arraycopy here + dst[j] = src[j]; + } + stmt.setString(i, new String(dst)); + } else if (type == String.class) { + stmt.setString(i, (String) parameter); + } else if (type.getEnumConstants() != null) { // if it's a enum value + stmt.setString(i, "" + parameter); + } else if (type == BigDecimal.class) { + stmt.setBigDecimal(i, (BigDecimal) parameter); + } else if (type == byte[].class) { + stmt.setBytes(i, (byte[]) parameter); + } else if (type == Byte[].class) { + // not efficient, will duplicate the array + final Byte[] src = (Byte[]) parameter; + final byte[] dst = new byte[src.length]; + for (int j = 0; j < src.length; j++) { // can't use System.arraycopy here + dst[j] = src[j]; + } + stmt.setBytes(i, dst); + } else if (parameter instanceof java.io.Reader) { + final java.io.Reader reader = (java.io.Reader) parameter; + + // the jdbc api for setCharacterStream requires the number + // of characters to be read so this will end up reading + // data twice (here and inside the jdbc driver) + // besides, the reader must support reset() + int size = 0; + try { + reader.reset(); + while (reader.read() != -1) { + size++; + } + reader.reset(); + } catch (IOException e) { + throw new RuntimeIOException(e); + } + stmt.setCharacterStream(i, reader, size); + } else if (parameter instanceof java.io.InputStream) { + final java.io.InputStream inputStream = (java.io.InputStream) parameter; + + // the jdbc api for setBinaryStream requires the number of + // bytes to be read so this will end up reading the stream + // twice (here and inside the jdbc driver) + // besides, the stream must support reset() + int size = 0; + try { + inputStream.reset(); + while (inputStream.read() != -1) { + size++; + } + inputStream.reset(); + } catch (IOException e) { + throw new RuntimeIOException(e); + } + stmt.setBinaryStream(i, inputStream, size); + } else if (parameter instanceof Clob) { + stmt.setClob(i, (Clob) parameter); + } else if (parameter instanceof Blob) { + stmt.setBlob(i, (Blob) parameter); + } else if (type == java.util.Date.class) { + final java.util.Date date = (java.util.Date) parameter; + stmt.setTimestamp(i, new java.sql.Timestamp(date.getTime())); + } else if (type == java.sql.Date.class) { + stmt.setDate(i, (java.sql.Date) parameter); + } else if (type == java.sql.Time.class) { + stmt.setTime(i, (java.sql.Time) parameter); + } else if (type == java.sql.Timestamp.class) { + stmt.setTimestamp(i, (java.sql.Timestamp) parameter); + } else { + // last resort; this should cover all database-specific + // object types + stmt.setObject(i, parameter); + } + + if (Log.isDebugEnabled(Log.PARAMETERS)) { + Log.debug(Log.PARAMETERS, "PreparedStatement [" + stmt + "] Parameter [" + i + "] type [" + + type.getSimpleName() + "] set to [" + Log.objectToString(parameter) + "]"); + } + + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + } + + /** + * Returns true if the provided class is a type supported natively (as + * opposed to a bean). + * + * @param type {@link java.lang.Class} type to be tested + * @since 1.0 + */ + private static boolean isNativeType(final Class type) { + + // to return an arbitrary object use Object.class + + return (type == boolean.class || type == Boolean.class || type == byte.class || type == Byte.class + || type == short.class || type == Short.class || type == int.class || type == Integer.class + || type == long.class || type == Long.class || type == float.class || type == Float.class + || type == double.class || type == Double.class || type == char.class || type == Character.class + || type == byte[].class || type == Byte[].class || type == char[].class || type == Character[].class + || type == String.class || type == BigDecimal.class || type == java.util.Date.class + || type == java.sql.Date.class || type == java.sql.Time.class || type == java.sql.Timestamp.class + || type == java.io.InputStream.class || type == java.io.Reader.class || type == java.sql.Clob.class + || type == java.sql.Blob.class || type == Object.class); + } + + /** + * Reads a column from the current row in the provided + * {@link java.sql.ResultSet} and returns an instance of the specified Java + * {@link java.lang.Class} containing the values read. + *

+ * This method is used while converting {@link java.sql.ResultSet} rows to + * objects. The class type is the field type in the target bean. + *

+ * Correspondence between class types and ResultSet.get methods is as + * follows: + *

    + *
  • Boolean/boolean: getBoolean + *
  • Byte/byte: getByte + *
  • Short/short: getShort + *
  • Integer/int: getInt + *
  • Long/long: getLong + *
  • Float/float: getFloat + *
  • Double/double: getDouble + *
  • Character/char: getString + *
  • Character[]/char[]: getString + *
  • Byte[]/byte[]: setBytes + *
  • String: setString + *
  • java.math.BigDecimal: getBigDecimal + *
  • java.io.Reader: getCharacterStream + *
  • java.io.InputStream: getBinaryStream + *
  • java.util.Date: getTimestamp + *
  • java.sql.Date: getDate + *
  • java.sql.Time: getTime + *
  • java.sql.Timestamp: getTimestamp + *
  • java.sql.Clob: getClob + *
  • java.sql.Blob: getBlob + *
+ *

+ * null's will be respected for any non-native types. This means that if a + * field is of type Integer it will be able to receive a null value from the + * ResultSet; on the other hand, if a field is of type int it will receive 0 + * for a null value from the {@link java.sql.ResultSet}. + * + * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be + * processed) + * @param column column index in the result set (starting with 1) + * @param type {@link java.lang.Class} of the object to be returned + * @since 1.0 + */ + public static Object getValueFromResultSet(final ResultSet resultSet, final int column, final Class type) { + + Object value = null; + + try { + + if (type == boolean.class) { + value = resultSet.getBoolean(column); + } else if (type == Boolean.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column); + } else if (type == byte.class) { + value = resultSet.getByte(column); + } else if (type == Byte.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getByte(column); + } else if (type == short.class) { + value = resultSet.getShort(column); + } else if (type == Short.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getShort(column); + } else if (type == int.class) { + value = resultSet.getInt(column); + } else if (type == Integer.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); + } else if (type == long.class) { + value = resultSet.getLong(column); + } else if (type == Long.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getLong(column); + } else if (type == float.class) { + value = resultSet.getFloat(column); + } else if (type == Float.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column); + } else if (type == double.class) { + value = resultSet.getDouble(column); + } else if (type == Double.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); + } else if (type == BigDecimal.class) { + value = resultSet.getObject(column) == null ? null : resultSet.getBigDecimal(column); + } else if (type == String.class) { + value = resultSet.getString(column); + } else if (type == Character.class || type == char.class) { + final String str = resultSet.getString(column); + if (str != null && str.length() > 1) { + throw new PersistException("Column [" + column + "] returned a string with length [" + + str.length() + "] but field type [" + type.getSimpleName() + + "] can only accept 1 character"); + } + value = (str == null || str.length() == 0) ? null : str.charAt(0); + } else if (type == byte[].class || type == Byte[].class) { + value = resultSet.getBytes(column); + } else if (type == char[].class || type == Character[].class) { + final String str = resultSet.getString(column); + value = (str == null) ? null : str.toCharArray(); + } else if (type == java.util.Date.class) { + final java.sql.Timestamp timestamp = resultSet.getTimestamp(column); + value = (timestamp == null) ? null : new java.util.Date(timestamp.getTime()); + } else if (type == java.sql.Date.class) { + value = resultSet.getDate(column); + } else if (type == java.sql.Time.class) { + value = resultSet.getTime(column); + } else if (type == java.sql.Timestamp.class) { + value = resultSet.getTimestamp(column); + } else if (type == java.io.InputStream.class) { + value = resultSet.getBinaryStream(column); + } else if (type == java.io.Reader.class) { + value = resultSet.getCharacterStream(column); + } else if (type == java.sql.Clob.class) { + value = resultSet.getClob(column); + } else if (type == java.sql.Blob.class) { + value = resultSet.getBlob(column); + } else { + // this will work for database-specific types + value = resultSet.getObject(column); + } + + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + + if (Log.isDebugEnabled(Log.RESULTS)) { + Log.debug(Log.RESULTS, "Read ResultSet [" + resultSet + "] column [" + column + "]" + + (value == null ? "" : " type [" + value.getClass().getSimpleName() + "]") + " value [" + + Log.objectToString(value) + "]"); + } + + return value; + } + + /** + * Reads a column from the current row in the provided + * {@link java.sql.ResultSet} and return a value correspondent to the SQL + * type provided (as defined in {@link java.sql.Types java.sql.Types}). + *

+ * This method is used while converting results sets to maps. The SQL type + * comes from the {@link java.sql.ResultSetMetaData ResultSetMetaData} for a + * given column. + *

+ * Correspondence between {@link java.sql.Types java.sql.Types} and + * ResultSet.get methods is as follows: + *

    + *
  • ARRAY: getArray + *
  • BIGINT: getLong + *
  • BIT: getBoolean + *
  • BLOB: getBytes + *
  • BOOLEAN: getBoolean + *
  • CHAR: getString + *
  • CLOB: getString + *
  • DATALINK: getBinaryStream + *
  • DATE: getDate + *
  • DECIMAL: getBigDecimal + *
  • DOUBLE: getDouble + *
  • FLOAT: getFloat + *
  • INTEGER: getInt + *
  • JAVA_OBJECT: getObject + *
  • LONGVARBINARY: getBytes + *
  • LONGVARCHAR: getString + *
  • NULL: getNull + *
  • NCHAR: getString + *
  • NUMERIC: getBigDecimal + *
  • OTHER: getObject + *
  • REAL: getDouble + *
  • REF: getRef + *
  • SMALLINT: getInt + *
  • TIME: getTime + *
  • TIMESTAMP: getTimestamp + *
  • TINYINT: getInt + *
  • VARBINARY: getBytes + *
  • VARCHAR: getString + *
  • [Oracle specific] 100: getFloat + *
  • [Oracle specific] 101: getDouble + *
+ *

+ * null's are respected for all types. This means that if a column is of + * type LONG and its value comes from the database as null, this method will + * return null for it. + * + * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be + * processed) + * @param column Column index in the result set (starting with 1) + * @param type type of the column (as defined in + * {@link java.sql.Types java.sql.Types}) + * @since 1.0 + */ + public static Object getValueFromResultSet(final ResultSet resultSet, final int column, final int type) { + + Object value = null; + + try { + + if (type == java.sql.Types.ARRAY) { + value = resultSet.getArray(column); + } else if (type == java.sql.Types.BIGINT) { + value = resultSet.getObject(column) == null ? null : resultSet.getLong(column); + } else if (type == java.sql.Types.BINARY) { + value = resultSet.getBytes(column); + } else if (type == java.sql.Types.BIT) { + value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column); + } else if (type == java.sql.Types.BLOB) { + value = resultSet.getBytes(column); + } else if (type == java.sql.Types.BOOLEAN) { + value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column); + } else if (type == java.sql.Types.CHAR) { + value = resultSet.getString(column); + } else if (type == java.sql.Types.CLOB) { + value = resultSet.getString(column); + } else if (type == java.sql.Types.DATALINK) { + value = resultSet.getBinaryStream(column); + } else if (type == java.sql.Types.DATE) { + value = resultSet.getDate(column); + } else if (type == java.sql.Types.DECIMAL) { + value = resultSet.getBigDecimal(column); + } else if (type == java.sql.Types.DOUBLE) { + value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); + } else if (type == java.sql.Types.FLOAT) { + value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column); + } else if (type == java.sql.Types.INTEGER) { + value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); + } else if (type == java.sql.Types.JAVA_OBJECT) { + value = resultSet.getObject(column); + } else if (type == java.sql.Types.LONGVARBINARY) { + value = resultSet.getBytes(column); + } else if (type == java.sql.Types.LONGVARCHAR) { + value = resultSet.getString(column); + } else if (type == java.sql.Types.NULL) { + value = null; + } else if (type == java.sql.Types.NUMERIC) { + value = resultSet.getBigDecimal(column); + } else if (type == java.sql.Types.OTHER) { + value = resultSet.getObject(column); + } else if (type == java.sql.Types.REAL) { + value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); + } else if (type == java.sql.Types.REF) { + value = resultSet.getRef(column); + } else if (type == java.sql.Types.SMALLINT) { + value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); + } else if (type == java.sql.Types.TIME) { + value = resultSet.getTime(column); + } else if (type == java.sql.Types.TIMESTAMP) { + value = resultSet.getTimestamp(column); + } else if (type == java.sql.Types.TINYINT) { + value = resultSet.getObject(column) == null ? null : resultSet.getInt(column); + } else if (type == java.sql.Types.VARBINARY) { + value = resultSet.getBytes(column); + } else if (type == java.sql.Types.VARCHAR) { + value = resultSet.getString(column); + } + + // oracle specific + else if (type == 100) { + value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column); + } else if (type == 101) { + value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column); + } else { + throw new PersistException("Could not get value for result set using type [" + + Log.sqlTypeToString(type) + "] on column [" + column + "]"); + } + + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + + if (Log.isDebugEnabled(Log.RESULTS)) { + Log.debug(Log.RESULTS, "Read ResultSet [" + resultSet + "] column [" + column + "] sql type [" + + Log.sqlTypeToString(type) + "]" + + (value == null ? "" : " type [" + value.getClass().getSimpleName() + "]") + " value [" + + Log.objectToString(value) + "]"); + } + + return value; + } + + /** + * Returns a list of values for the fields in the provided object that match + * the provided list of columns according with the object mapping. + * + * @param object source object to obtain parameter values + * @param columns name of the database columns to get parameters for + * @param mapping mapping for the object class + * @since 1.0 + */ + private static Object[] getParametersFromObject(final Object object, final String[] columns, + final TableMapping mapping) { + + Object[] parameters = new Object[columns.length]; + for (int i = 0; i < columns.length; i++) { + final String columnName = columns[i]; + final Method getter = mapping.getGetterForColumn(columnName); + + Object value = null; + try { + value = getter.invoke(object, new Object[]{}); + } catch (Exception e) { + throw new PersistException("Could not access getter for column [" + columnName + "]", e); + } + + parameters[i] = value; + } + + return parameters; + } + + /** + * Reads a row from the provided {@link java.sql.ResultSet} and converts it + * to an object instance of the given class. + *

+ * See {@link #getValueFromResultSet(ResultSet, int, Class)} for details on + * the mappings between ResultSet.get methods and Java types. + * + * @param objectClass type of the object to be returned + * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be + * processed) + * @see #isNativeType(Class) + * @see #getValueFromResultSet(ResultSet, int, Class) + * @since 1.0 + */ + public Object loadObject(final Class objectClass, final ResultSet resultSet) throws SQLException { + + final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + + Object ret = null; + + // for native objects (int, long, String, Date, etc.) + if (isNativeType(objectClass)) { + if (resultSetMetaData.getColumnCount() != 1) { + throw new PersistException("ResultSet returned [" + resultSetMetaData.getColumnCount() + + "] columns but 1 column was expected to load data into an instance of [" + + objectClass.getName() + "]"); + } + ret = getValueFromResultSet(resultSet, 1, objectClass); + } + + // for beans + else { + + final Mapping mapping = getMapping(objectClass); + + try { + ret = objectClass.newInstance(); + } catch (Exception e) { + throw new PersistException(e); + } + + for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { + final String columnName = resultSetMetaData.getColumnName(i).toLowerCase(); + final Method setter = mapping.getSetterForColumn(columnName); + if (setter == null) { + throw new PersistException("Column [" + columnName + + "] from result set does not have a mapping to a field in [" + + objectClass.getName() + "]"); + } + + Class type = setter.getParameterTypes()[0]; + Object value = getValueFromResultSet(resultSet, i, type); + + // If this is an enum do a case insensitive comparison + if (type.isEnum()) { + Object[] enumConstants = type.getEnumConstants(); + for (Object element : enumConstants) { + if (("" + value).equalsIgnoreCase(element.toString())) { + value = element; + break; + } + } + } + try { + setter.invoke(ret, new Object[]{value}); + } catch (Exception e) { + throw new PersistException("Error setting value [" + value + "]" + + (value == null ? "" : " of type [" + value.getClass().getName() + "]") + " from column [" + + columnName + "] using setter [" + setter + "]: " + e.getMessage(), e); + } + } + + if (ret instanceof PersistableObject) { + // Save this object's initial state to later detect changed properties + ((PersistableObject) ret).saveReadState(); + } + + } + return ret; + } + + /** + * Reads a row from the provided {@link java.sql.ResultSet} and converts it + * to a map having the column names as keys and results as values. + *

+ * See {@link #getValueFromResultSet(ResultSet, int, int)} for details on + * the mappings between ResultSet.get methods and + * {@link java.sql.Types java.sql.Types} types + * + * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be + * processed) + * @since 1.0 + */ + public static Map loadMap(final ResultSet resultSet) throws SQLException { + + final Map ret = new LinkedHashMap(); + final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + + for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { + final String columnName = resultSetMetaData.getColumnName(i).toLowerCase(); + final int type = resultSetMetaData.getColumnType(i); + final Object value = getValueFromResultSet(resultSet, i, type); + ret.put(columnName, value); + } + + return ret; + } + + /** + * Set auto-generated keys (returned from an insert operation) into an + * object. + * + * @param object {@link java.lang.Object} to have fields mapped to + * auto-generated keys set + * @param result {@link Result} containing auto-generated keys + * @since 1.0 + */ + public void setAutoGeneratedKeys(final Object object, final Result result) { + + if (result.getGeneratedKeys().size() != 0) { + final TableMapping mapping = getTableMapping(object.getClass(), "setAutoGeneratedKeys()"); + for (int i = 0; i < mapping.getAutoGeneratedColumns().length; i++) { + final String columnName = mapping.getAutoGeneratedColumns()[i]; + final Method setter = mapping.getSetterForColumn(columnName); + final Object key = result.getGeneratedKeys().get(i); + try { + setter.invoke(object, new Object[]{key}); + } catch (Exception e) { + throw new PersistException("Could not invoke setter [" + setter + "] with auto generated key [" + + key + "] of class [" + key.getClass().getName() + "]", e); + } + } + } + + } + + // ---------- execute ---------- + + /** + * Executes an update and return a {@link Result} object containing the + * number of rows modified and auto-generated keys produced. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @param objectClass Class of the object related with the query. Used to + * determine the types of the auto-incremented keys. Only important if + * autoGeneratedKeys contains values. + * @param sql SQL code to be executed. + * @param autoGeneratedKeys List of columns that are going to be + * auto-generated during the query execution. + * @param parameters Parameters to be used in the PreparedStatement. + * @since 1.0 + */ + public Result executeUpdate(final Class objectClass, final String sql, final String[] autoGeneratedKeys, + final Object... parameters) { + + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + final PreparedStatement stmt = getPreparedStatement(sql, autoGeneratedKeys); + + try { + setParameters(stmt, parameters); + + int rowsModified = 0; + try { + rowsModified = stmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters " + + Arrays.toString(parameters) + ": " + e.getMessage(), e); + } + + final List generatedKeys = new ArrayList(); + if (autoGeneratedKeys.length != 0) { + try { + final Mapping mapping = getMapping(objectClass); + final ResultSet resultSet = stmt.getGeneratedKeys(); + for (int i = 0; i < autoGeneratedKeys.length; i++) { + resultSet.next(); + + // get the auto-generated key using the ResultSet.get method + // that matches + // the bean setter parameter type + final Method setter = mapping.getSetterForColumn(autoGeneratedKeys[i]); + final Class type = setter.getParameterTypes()[0]; + final Object value = Persist.getValueFromResultSet(resultSet, 1, type); + + generatedKeys.add(value); + } + resultSet.close(); + } catch (SQLException e) { + throw new RuntimeSQLException("This JDBC driver does not support PreparedStatement.getGeneratedKeys()." + + " Please use setUpdateAutoGeneratedKeys(false) in your Persist instance" + + " to disable attempts to use that feature"); + } + } + + Result result = new Result(rowsModified, generatedKeys); + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "executeUpdate in [" + (end - begin) + "ms] for sql [" + sql + "]"); + } + + return result; + } finally { + closePreparedStatement(stmt); + } + } + + /** + * Executes an update and returns the number of rows modified. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @param sql SQL code to be executed. + * @param parameters Parameters to be used in the PreparedStatement. + * @since 1.0 + */ + public int executeUpdate(final String sql, final Object... parameters) { + + final PreparedStatement stmt = getPreparedStatement(sql); + int rowsModified = 0; + + try { + setParameters(stmt, parameters); + rowsModified = stmt.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters " + + Arrays.toString(parameters) + ": " + e.getMessage(), e); + } finally { + closePreparedStatement(stmt); + } + + return rowsModified; + } + + /** + * Execute an arbitrary SQL statement. + * + * @param sql any arbitrary SQL statement + */ + public void execute(final String sql) { + + Statement stmt = null; + try { + stmt = connection.createStatement(); + stmt.execute(sql); + } catch (SQLException e) { + throw new RuntimeSQLException("Error executing sql [" + sql + "] : " + e.getMessage(), e); + } finally { + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + } + } + } + } + + /** + * Execute an arbitrary SQL statement with parameters. + * + * @param sql any arbitrary SQL statement + */ + public void execute(final String sql, final Object... parameters) { + + final PreparedStatement stmt = getPreparedStatement(sql); + try { + setParameters(stmt, parameters); + stmt.execute(); + } catch (SQLException e) { + throw new RuntimeSQLException("Error executing sql [" + sql + "] : " + e.getMessage(), e); + } finally { + closePreparedStatement(stmt); + } + } + + // ---------- insert ---------- + + /** + * Inserts an object into the database. + * TODO somehow I don't see the ID set after an insert with the specified object using h2-1.3.159.jar + * + * @since 1.0 + */ + public int insert(final Object object) { + final TableMapping mapping = getTableMapping(object.getClass(), "insert()"); + final String sql = mapping.getInsertSql(); + final String[] columns = mapping.getNotAutoGeneratedColumns(); + final Object[] parameters = getParametersFromObject(object, columns, mapping); + + int ret = 0; + if (updateAutoGeneratedKeys) { + if (mapping.supportsGetGeneratedKeys()) { + final Result result = executeUpdate(object.getClass(), sql, mapping.getAutoGeneratedColumns(), + parameters); + setAutoGeneratedKeys(object, result); + ret = result.getRowsModified(); + } else { + throw new PersistException("While inserting instance of [" + object.getClass().getName() + + "] autoUpdateGeneratedKeys is set to [true] but the database doesn't support this feature"); + } + } else { + ret = executeUpdate(sql, parameters); + } + return ret; + } + + /** + * Inserts a batch of objects into the database. + * + * @since 1.0 + */ + // TODO: use batch updates + public int[] insertBatch(final Object... objects) { + if (objects == null || objects.length == 0) { + return new int[0]; + } + final int[] results = new int[objects.length]; + for (int i = 0; i < objects.length; i++) { + results[i] = insert(objects[i]); + } + return results; + } + + // ---------- update ---------- + + /** + * Updates an object in the database. The object will be identified using + * its mapped table's primary key. If no primary keys are defined in the + * mapped table, a {@link PersistException} will be thrown. + * + * @since 1.0 + */ + public int update(final Object object) { + final TableMapping mapping = getTableMapping(object.getClass(), "update()"); + + if (mapping.getPrimaryKeys().length == 0) { + throw new PersistException("Table " + mapping.getTableName() + " doesn't have a primary key"); + } + final String sql; + final Object[] parameters; + + if (object instanceof PersistableObject) { + + UpdateInfo info = mapping.getCurrentUpdateInfo(object); + + if (info.sql.trim().length() == 0) { + Log.warn(Log.ENGINE, "Object unchanged no update will be executed."); + return 0; + } + sql = info.sql; + parameters = getParametersFromObject(object, info.columns, mapping); + + } else { + + sql = mapping.getUpdateSql(); + final String[] columns = new String[mapping.getNotPrimaryKeys().length + mapping.getPrimaryKeys().length]; + int i = 0; + for (String notPrimaryKey : mapping.getNotPrimaryKeys()) { + columns[i++] = notPrimaryKey; + } + for (String primaryKey : mapping.getPrimaryKeys()) { + columns[i++] = primaryKey; + } + parameters = getParametersFromObject(object, columns, mapping); + } + + if (Log.isDebugEnabled(Log.ENGINE)) { + Log.debug(Log.ENGINE, sql + " Params: " + Arrays.asList(parameters)); + } + + int rowsUpdated = executeUpdate(sql, parameters); + if (object instanceof PersistableObject) { + // Set the original value to this new state. + PersistableObject wo = (PersistableObject) object; + wo.originalValue = null; + wo.saveReadState(); + } + return rowsUpdated; + } + + /** + * Updates a batch of objects in the database. The objects will be + * identified using their mapped table's primary keys. If no primary keys + * are defined in the mapped table, a {@link PersistException} will be + * thrown. + * + * @since 1.0 + */ + public int[] updateBatch(final Object... objects) { + // TODO: use batch updates + if (objects == null || objects.length == 0) { + return new int[0]; + } + int[] results = new int[objects.length]; + for (int i = 0; i < objects.length; i++) { + results[i] = update(objects[i]); + } + return results; + } + + // ---------- delete ---------- + + /** + * Deletes an object in the database. The object will be identified using + * its mapped table's primary key. If no primary keys are defined in the + * mapped table, a PersistException will be thrown. + * + * @since 1.0 + */ + public int delete(final Object object) { + final TableMapping mapping = getTableMapping(object.getClass(), "delete()"); + if (mapping.getPrimaryKeys().length == 0) { + throw new PersistException("Table " + mapping.getTableName() + " doesn't have a primary key"); + } + final String sql = mapping.getDeleteSql(); + final String[] columns = mapping.getPrimaryKeys(); + final Object[] parameters = getParametersFromObject(object, columns, mapping); + return executeUpdate(sql, parameters); + } + + /** + * Updates a batch of objects in the database. The objects will be + * identified using their matched table's primary keys. If no primary keys + * are defined in a given object, a PersistException will be thrown. + * + * @since 1.0 + */ + public int[] deleteBatch(final Object... objects) { + // TODO: use batch updates + if (objects == null || objects.length == 0) { + return new int[0]; + } + int[] results = new int[objects.length]; + for (int i = 0; i < objects.length; i++) { + results[i] = delete(objects[i]); + } + return results; + } + + // ---------- read ---------- + + // --- single objects --- + + /** + * Reads a single object from the database by mapping the results of the SQL + * query into an instance of the given object class. Only the columns + * returned from the SQL query will be set into the object instance. If a + * given column can't be mapped to the target object instance, a + * {@link PersistException} will be thrown. + * + * @since 1.0 + */ + public T read(final Class objectClass, final String sql) { + return read(objectClass, sql, (Object[]) null); + } + + /** + * Reads a single object from the database by mapping the results of the + * parameterized SQL query into an instance of the given object class. Only + * the columns returned from the SQL query will be set into the object + * instance. If a given column can't be mapped to the target object + * instance, a {@link PersistException} will be thrown. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public T read(final Class objectClass, final String sql, final Object... parameters) { + final PreparedStatement stmt = getPreparedStatement(sql); + return read(objectClass, stmt, parameters); + } + + /** + * Reads a single object from the database by mapping the results of the + * execution of the given PreparedStatement into an instance of the given + * object class. Only the columns returned from the PreparedStatement + * execution will be set into the object instance. If a given column can't + * be mapped to the target object instance, a {@link PersistException} + * will be thrown. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public T read(final Class objectClass, final PreparedStatement statement, final Object... parameters) { + try { + setParameters(statement, parameters); + final ResultSet resultSet = statement.executeQuery(); + final T ret = read(objectClass, resultSet); + return ret; + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } finally { + if (closePreparedStatementsAfterRead) { + closePreparedStatement(statement); + } + } + } + + /** + * Reads a single object from the database by mapping the content of the + * ResultSet current row into an instance of the given object class. Only + * columns contained in the ResultSet will be set into the object instance. + * If a given column can't be mapped to the target object instance, a + * PersistException will be thrown. + * + * @since 1.0 + */ + public T read(final Class objectClass, final ResultSet resultSet) { + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + Object ret = null; + try { + if (resultSet.next()) { + ret = loadObject(objectClass, resultSet); + if (resultSet.next()) { + throw new PersistException("Non-unique result returned"); + } + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "read in [" + (end - begin) + "ms] for object type [" + + objectClass.getSimpleName() + "]"); + } + + return (T) ret; + } + + /** + * Reads an object from the database by its primary keys. + * + * @since 1.0 + */ + public T readByPrimaryKey(final Class objectClass, final Object... primaryKeyValues) { + final TableMapping mapping = getTableMapping(objectClass, "readByPrimaryKey()"); + final String sql = mapping.getSelectSql(); + return read(objectClass, sql, primaryKeyValues); + } + + // --- lists --- + + /** + * Reads a list of objects from the database by mapping the content of the + * ResultSet into instances of the given object class. Only columns + * contained in the ResultSet will be set into the object instances. If a + * given column can't be mapped to a target object instance, a + * PersistException will be thrown. + * + * @since 1.0 + */ + public List readList(final Class objectClass, final ResultSet resultSet) { + + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + final List ret = new ArrayList(); + try { + while (resultSet.next()) { + ret.add((T) loadObject(objectClass, resultSet)); + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "readList in [" + (end - begin) + "ms] for object type [" + + objectClass.getSimpleName() + "]"); + } + + return ret; + } + + /** + * Reads a list of objects from the database by mapping the results of the + * execution of the given PreparedStatement into instances of the given + * object class. Only the columns returned from the PreparedStatement + * execution will be set into the object instances. If a given column can't + * be mapped to a target object instance, a PersistException will be + * thrown. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public List readList(final Class objectClass, final PreparedStatement statement, + final Object... parameters) { + setParameters(statement, parameters); + try { + final ResultSet resultSet = statement.executeQuery(); + return readList(objectClass, resultSet); + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Reads a list of objects from the database by mapping the results of the + * parameterized SQL query into instances of the given object class. Only + * the columns returned from the SQL query will be set into the object + * instance. If a given column can't be mapped to the target object + * instance, a {@link PersistException} will be thrown. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public List readList(final Class objectClass, final String sql, final Object... parameters) { + final PreparedStatement stmt = getPreparedStatement(sql); + try { + return readList(objectClass, stmt, parameters); + } finally { + if (closePreparedStatementsAfterRead) { + closePreparedStatement(stmt); + } + } + } + + /** + * Reads a list of objects from the database by mapping the results of the + * SQL query into instances of the given object class. Only the columns + * returned from the SQL query will be set into the object instance. If a + * given column can't be mapped to the target object instance, a + * {@link PersistException} will be thrown. + * + * @since 1.0 + */ + public List readList(final Class objectClass, final String sql) { + return readList(objectClass, sql, (Object[]) null); + } + + /** + * Reads a list of all objects in the database mapped to the given object + * class. + * + * @since 1.0 + */ + public List readList(final Class objectClass) { + final TableMapping mapping = getTableMapping(objectClass, "readList(Class)"); + final String sql = mapping.getSelectAllSql(); + return readList(objectClass, sql); + } + + // --- iterators --- + + /** + * Returns an {@link java.util.Iterator} for a list of objects from the + * database that map the contents of the ResultSet into instances of the + * given object class. Only columns contained in the ResultSet will be set + * into the object instances. If a given column can't be mapped to a target + * object instance, a {@link PersistException} will be thrown. + * + * @since 1.0 + */ + public Iterator readIterator(final Class objectClass, final ResultSet resultSet) { + + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + final ResultSetIterator i = new ResultSetIterator(this, objectClass, resultSet, ResultSetIterator.TYPE_OBJECT); + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "readIterator in [" + (end - begin) + "ms] for object type [" + + objectClass.getSimpleName() + "]"); + } + + return i; + } + + /** + * Returns an {@link java.util.Iterator} for a list of objects from the + * database that map the contents of the execution of the given + * PreparedStatement into instances of the given object class. Only columns + * contained in the ResultSet will be set into the object instances. If a + * given column can't be mapped to a target object instance, a + * PersistException will be thrown. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public Iterator readIterator(final Class objectClass, final PreparedStatement statement, + final Object... parameters) { + setParameters(statement, parameters); + try { + final ResultSet resultSet = statement.executeQuery(); + return readIterator(objectClass, resultSet); + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Returns an {@link java.util.Iterator} for a list of objects from the + * database that map the contents of the execution of the given SQL query + * into instances of the given object class. Only columns contained in the + * ResultSet will be set into the object instances. If a given column can't + * be mapped to a target object instance, a {@link PersistException} will + * be thrown. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public Iterator readIterator(final Class objectClass, final String sql, final Object... parameters) { + final PreparedStatement stmt = getPreparedStatement(sql); + final Iterator ret = readIterator(objectClass, stmt, parameters); + // don't close the prepared statement otherwise the result set in the + // iterator will be closed + return ret; + } + + /** + * Returns an {@link java.util.Iterator} for a list of objects from the + * database that map the contents of the execution of the given SQL query + * into instances of the given object class. Only columns contained in the + * ResultSet will be set into the object instances. If a given column can't + * be mapped to a target object instance, a {@link PersistException} will + * be thrown. + * + * @since 1.0 + */ + public Iterator readIterator(final Class objectClass, final String sql) { + return readIterator(objectClass, sql, (Object[]) null); + } + + /** + * Returns an {@link java.util.Iterator} for a list of all objects in the + * database mapped to the given object class. + * + * @since 1.0 + */ + public Iterator readIterator(final Class objectClass) { + final TableMapping mapping = getTableMapping(objectClass, "readIterator(Class)"); + final String sql = mapping.getSelectAllSql(); + return readIterator(objectClass, sql); + } + + // ---------- read (map) ---------- + + // --- single objects --- + + /** + * Reads a single object from the database by mapping the results of the SQL + * query into an instance of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + * + * @since 1.0 + */ + public Map readMap(final String sql) { + return readMap(sql, (Object[]) null); + } + + /** + * Reads a single object from the database by mapping the results of the SQL + * query into an instance of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + *

+ * Parameters will be set according with the correspondence defined in + * {@link Persist#setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public Map readMap(final String sql, final Object... parameters) { + final PreparedStatement stmt = getPreparedStatement(sql); + try { + return readMap(stmt, parameters); + } finally { + if (closePreparedStatementsAfterRead) { + closePreparedStatement(stmt); + } + } + } + + /** + * Reads a single object from the database by mapping the results of the + * PreparedStatement execution into an instance of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + *

+ * Parameters will be set according with the correspondence defined in + * {@link Persist#setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public Map readMap(final PreparedStatement statement, final Object... parameters) { + setParameters(statement, parameters); + try { + final ResultSet resultSet = statement.executeQuery(); + return readMap(resultSet); + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Reads a single object from the database by mapping the results of the + * current ResultSet row into an instance of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + * + * @since 1.0 + */ + public Map readMap(final ResultSet resultSet) { + + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + Map ret = null; + + try { + if (resultSet.next()) { + ret = loadMap(resultSet); + if (resultSet.next()) { + throw new PersistException("Non-unique result returned"); + } + } else { + ret = null; + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "readMap in [" + (end - begin) + "ms]"); + } + + return ret; + } + + // --- list --- + + /** + * Reads a list of objects from the database by mapping the ResultSet rows + * to instances of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + * + * @since 1.0 + */ + public List> readMapList(final ResultSet resultSet) { + + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + final List ret = new ArrayList(); + try { + while (resultSet.next()) { + ret.add(loadMap(resultSet)); + } + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "readMapList [" + (end - begin) + "ms]"); + } + + return ret; + } + + /** + * Reads a list of objects from the database by mapping the + * PreparedStatement execution results to instances of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public List> readMapList(final PreparedStatement statement, final Object... parameters) { + setParameters(statement, parameters); + try { + final ResultSet resultSet = statement.executeQuery(); + return readMapList(resultSet); + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Reads a list of objects from the database by mapping the SQL execution + * results to instances of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public List> readMapList(final String sql, final Object... parameters) { + final PreparedStatement stmt = getPreparedStatement(sql); + try { + return readMapList(stmt, parameters); + } finally { + if (closePreparedStatementsAfterRead) { + closePreparedStatement(stmt); + } + } + } + + /** + * Reads a list of all objects in the database mapped to the given object + * class and return each result as an instance of {@link java.util.Map}. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + * + * @since 1.0 + */ + public List> readMapList(final String sql) { + return readMapList(sql, (Object[]) null); + } + + // --- iterator --- + + /** + * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} + * instances containing data from the provided ResultSet rows. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + * + * @since 1.0 + */ + public Iterator readMapIterator(final ResultSet resultSet) { + return new ResultSetIterator(this, null, resultSet, ResultSetIterator.TYPE_MAP); + } + + /** + * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} + * instances containing data from the execution of the provided + * PreparedStatement. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + *

+ * Parameters will be set according with the correspondence defined in + * {@link Persist#setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public Iterator readMapIterator(final PreparedStatement statement, final Object... parameters) { + setParameters(statement, parameters); + try { + final ResultSet resultSet = statement.executeQuery(); + return readMapIterator(resultSet); + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + + /** + * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} + * instances containing data from the execution of the provided parametrized + * SQL. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + *

+ * Parameters will be set according with the correspondence defined in + * {@link #setParameters(PreparedStatement, int[], Object[])} + * + * @since 1.0 + */ + public Iterator readMapIterator(final String sql, final Object... parameters) { + + long begin = 0; + if (Log.isDebugEnabled(Log.PROFILING)) { + begin = System.currentTimeMillis(); + } + + final PreparedStatement stmt = getPreparedStatement(sql); + final Iterator ret = readMapIterator(stmt, parameters); + + if (Log.isDebugEnabled(Log.PROFILING)) { + final long end = System.currentTimeMillis(); + Log.debug(Log.PROFILING, "readMapIterator in [" + (end - begin) + "ms]"); + } + + return ret; + } + + /** + * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map} + * instances containing data from the execution of the provided SQL. + *

+ * Types returned from the database will be converted to Java types in the + * map according with the correspondence defined in + * {@link #getValueFromResultSet(ResultSet, int, int)}. + * + * @since 1.0 + */ + public Iterator readMapIterator(final String sql) { + return readMapIterator(sql, (Object[]) null); + } } diff --git a/src/main/net/sf/persist/PersistableObject.java b/src/main/net/sf/persist/PersistableObject.java new file mode 100644 index 0000000..d00b547 --- /dev/null +++ b/src/main/net/sf/persist/PersistableObject.java @@ -0,0 +1,36 @@ +package net.sf.persist; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * Abstract class to handle tracking property changes in your data objects. + * This is not required to persist your data objects however subclassing from + * PersistableObject will mean that only changed properties will be written + * back to the database. + * + * @author Dan Howard + * @since Dec 3, 2010 9:30:45 PM + */ +public abstract class PersistableObject { + + Object originalValue = null; + + final void saveReadState() throws PersistException { + try { + if (originalValue == null) { + Map[] map = Mapping.getFieldsMaps(getClass()); + Map getters = map[1]; + Map setters = map[2]; + + originalValue = getClass().newInstance(); + for (String key : getters.keySet()) { + setters.get(key).invoke(originalValue, getters.get(key).invoke(this)); + } + } + } catch (Exception e) { + throw new PersistException(e.getMessage(), e); + } + } + +} diff --git a/src/main/net/sf/persist/TableMapping.java b/src/main/net/sf/persist/TableMapping.java index cc3559e..076071e 100644 --- a/src/main/net/sf/persist/TableMapping.java +++ b/src/main/net/sf/persist/TableMapping.java @@ -2,443 +2,495 @@ package net.sf.persist; +import net.sf.persist.annotations.Column; +import net.sf.persist.annotations.Table; + import java.lang.reflect.Method; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Holds mapping data from a given class and a table */ public final class TableMapping extends Mapping { - private final Class objectClass; - - private final net.sf.persist.annotations.Table tableAnnotation; - private final String tableName; - - private final String[] fields; // list of fields which have getters and setters - private final Map annotationsMap; // maps field names to annotations - private final Map gettersMap; // maps field names to getters - private final Map settersMap; // maps field names to setters - - private final boolean supportsGetGeneratedKeys; - private final boolean supportsBatchUpdates; - - private final Map columnsMap = new LinkedHashMap(); // maps table columns to property names - private final String[] columns; - private final String[] primaryKeys; - private final String[] notPrimaryKeys; - private final String[] autoGeneratedColumns; - private final String[] notAutoGeneratedColumns; - - private final String selectSql; - private final String selectAllSql; - private final String insertSql; - private final String updateSql; - private final String deleteSql; - - public TableMapping(final DatabaseMetaData metaData, final Class objectClass, final NameGuesser nameGuesser) - throws SQLException { - - ResultSet resultSet = null; - - // object class - this.objectClass = objectClass; - - // database support for auto increment keys - supportsGetGeneratedKeys = metaData.supportsGetGeneratedKeys(); - - // database support for batch updates - supportsBatchUpdates = metaData.supportsBatchUpdates(); - - // database name - final String databaseProductName = metaData.getDatabaseProductName(); - - // table annotation - tableAnnotation = (net.sf.persist.annotations.Table) objectClass - .getAnnotation(net.sf.persist.annotations.Table.class); - - // schema pattern - String schemaPattern = null; - if (databaseProductName.equalsIgnoreCase("Oracle")) { - schemaPattern = "%"; // oracle expects a pattern such as "%" to work - } - - // table name and annotation - tableName = getTableName(metaData, schemaPattern, objectClass, nameGuesser); - - // all column names and types (from db) - - final List columnsList = new ArrayList(); - resultSet = metaData.getColumns(null, schemaPattern, tableName, "%"); - while (resultSet.next()) { - final String columnName = resultSet.getString(4); - columnsList.add(columnName); - } - resultSet.close(); - columns = toArray(columnsList); - - // all primary keys (from db) - - final List primaryKeysList = new ArrayList(); - resultSet = metaData.getPrimaryKeys(null, schemaPattern, tableName); - while (resultSet.next()) { - final String columnName = resultSet.getString(4); - primaryKeysList.add(columnName); - } - resultSet.close(); - primaryKeys = toArray(primaryKeysList); - - // not primary keys - - final List notPrimaryKeysList = new ArrayList(); - for (String columnName : columns) { - if (!primaryKeysList.contains(columnName)) { - notPrimaryKeysList.add(columnName); - } - } - notPrimaryKeys = toArray(notPrimaryKeysList); - - // map field names to annotations, getters and setters - - final Map[] fieldsMaps = getFieldsMaps(objectClass); - annotationsMap = fieldsMaps[0]; - gettersMap = fieldsMaps[1]; - settersMap = fieldsMaps[2]; - fields = toArray(gettersMap.keySet()); - - // map column names to field names; create list of auto-increment columns - // columnsMap use keys in lower case - - // the actual autoGeneratedColumns list should have columns in the database order - final Set autoGeneratedColumnsTemp = new HashSet(); - for (String fieldName : fields) { - final String columnName = getColumnName(objectClass, nameGuesser, annotationsMap, columnsList, tableName, - fieldName); - columnsMap.put(columnName.toLowerCase(Locale.ENGLISH), fieldName); - final net.sf.persist.annotations.Column annotation = annotationsMap.get(fieldName); - if (annotation != null && annotation.autoGenerated()) { - autoGeneratedColumnsTemp.add(columnName); - } - } - - // auto-increment and not-auto-increment columns, in the database order - - final List notAutoGeneratedColumnsList = new ArrayList(); - final List autoGeneratedColumnsList = new ArrayList(); - for (String columnName : columns) { - if (autoGeneratedColumnsTemp.contains(columnName)) { - autoGeneratedColumnsList.add(columnName); - } else { - notAutoGeneratedColumnsList.add(columnName); - } - } - notAutoGeneratedColumns = toArray(notAutoGeneratedColumnsList); - autoGeneratedColumns = toArray(autoGeneratedColumnsList); - - // assemble sql blocks to be used by crud sql statements - - final String allColumns = join(columns, "", ","); - final String noAutoColumns = join(notAutoGeneratedColumns, "", ","); - final String allPlaceholders = multiply("?", columns.length, ","); - final String noAutoPlaceholders = multiply("?", notAutoGeneratedColumns.length, ","); - final String where = join(primaryKeys, "=?", " and "); - final String updateSet = join(notPrimaryKeys, "=?", ","); - - // assemble crud sql statements - - selectSql = "select " + allColumns + " from " + tableName + " where " + where; - selectAllSql = "select " + allColumns + " from " + tableName; - - if (autoGeneratedColumns.length == 0) { - insertSql = "insert into " + tableName + "(" + allColumns + ")values(" + allPlaceholders + ")"; - } else { - insertSql = "insert into " + tableName + "(" + noAutoColumns + ")values(" + noAutoPlaceholders + ")"; - } - - updateSql = "update " + tableName + " set " + updateSet + " where " + where; - deleteSql = "delete from " + tableName + " where " + where; - - } - - // ---------- getters and setters ---------- - - public boolean supportsGetGeneratedKeys() { - return supportsGetGeneratedKeys; - } - - public boolean supportsBatchUpdates() { - return supportsBatchUpdates; - } - - public Class getObjectClass() { - return objectClass; - } - - public String getTableName() { - return tableName; - } - - public net.sf.persist.annotations.Table getTableAnnotation() { - return tableAnnotation; - } - - public String[] getColumns() { - return columns; - } - - public Map getColumnsMap() { - return columnsMap; - } - - public String[] getPrimaryKeys() { - return primaryKeys; - } - - public String[] getNotPrimaryKeys() { - return notPrimaryKeys; - } - - public String[] getAutoGeneratedColumns() { - return autoGeneratedColumns; - } - - public String[] getNotAutoGeneratedColumns() { - return notAutoGeneratedColumns; - } - - public String[] getFields() { - return fields; - } - - public Map getAnnotationsMap() { - return annotationsMap; - } - - public Map getGettersMap() { - return gettersMap; - } - - public Map getSettersMap() { - return settersMap; - } - - public Method getGetterForColumn(final String columnName) { - final String fieldName = columnsMap.get(columnName.toLowerCase(Locale.ENGLISH)); - return gettersMap.get(fieldName); - } - - public Method getSetterForColumn(final String columnName) { - final String fieldName = columnsMap.get(columnName.toLowerCase(Locale.ENGLISH)); - return settersMap.get(fieldName); - } - - public String getSelectSql() { - return selectSql; - } - - public String getSelectAllSql() { - return selectAllSql; - } - - public String getInsertSql() { - return insertSql; - } - - public String getUpdateSql() { - return updateSql; - } - - public String getDeleteSql() { - return deleteSql; - } - - // ---------- utility methods ---------- - - private static String getTableName(final DatabaseMetaData metaData, final String schema, final Class objectClass, - final NameGuesser nameGuesser) throws SQLException { - - String name = null; - - final net.sf.persist.annotations.Table tableAnnotation = (net.sf.persist.annotations.Table) objectClass - .getAnnotation(net.sf.persist.annotations.Table.class); - - if (tableAnnotation != null && !tableAnnotation.name().equals("")) { - // if there's a Table annotation, use it - name = checkTableName(metaData, schema, tableAnnotation.name()); - - // test if the specified table name actually exists - if (name == null) { - throw new PersistException("Class [" + objectClass.getName() + "] specifies table [" - + tableAnnotation.name() + "] that does not exist in the database"); - } - } else { - // if no annotation, try guessed table names - final String className = objectClass.getSimpleName().substring(0, 1).toLowerCase() - + objectClass.getSimpleName().substring(1); - final Set guessedNames = nameGuesser.guessColumn(className); - for (String guessedTableName : guessedNames) { - name = checkTableName(metaData, schema, guessedTableName); - if (name != null) { - break; - } - } - if (name == null) { - throw new PersistException("Class [" + objectClass.getName() - + "] does not specify a table name through a Table annotation and no guessed table names " - + guessedNames + " exist in the database"); - } - } - - return name; - } - - /** - * Check if the given name corresponds to a table in the database and - * returns the corresponding name with the capitalization returned by the - * database metadata - */ - private static String checkTableName(final DatabaseMetaData metaData, final String schema, final String tableName) - throws SQLException { - - ResultSet resultSet; - String ret = null; - - // try name in upper case -- should work in most databases - resultSet = metaData.getTables(null, schema, tableName.toUpperCase(Locale.ENGLISH), null); - if (resultSet.next()) { - ret = tableName.toUpperCase(Locale.ENGLISH); - } - resultSet.close(); - - if (ret == null) { - // try name in lower case - resultSet = metaData.getTables(null, schema, tableName.toLowerCase(Locale.ENGLISH), null); - if (resultSet.next()) { - ret = tableName.toLowerCase(Locale.ENGLISH); - } - resultSet.close(); - } - - if (ret == null) { - // last resort: compare with all table names in the schema - // (may be very expensive in databases such as oracle) - // this may end up being used in databases that allow case sensitive names (such as postgresql) - resultSet = metaData.getTables(null, schema, "%", null); - while (resultSet.next()) { - final String dbTableName = resultSet.getString(3); - if (tableName.equalsIgnoreCase(dbTableName)) { - ret = dbTableName; - break; - } - } - resultSet.close(); - } - - return ret; - } - - private static String getColumnName(final Class objectClass, final NameGuesser nameGuesser, - final Map annotationsMap, final List columnsList, - final String tableName, final String fieldName) throws SQLException { - - String columnName = null; - - final net.sf.persist.annotations.Column annotation = annotationsMap.get(fieldName); - if (annotation != null && !annotation.name().equals("")) { - // if there's an annotation, use it - columnName = getIgnoreCase(columnsList, annotation.name()); - - // check if the specified column actually exists in the table - if (columnName == null) { - throw new PersistException("Field [" + fieldName + "] from class [" + objectClass.getName() - + "] specifies column [" + annotation.name() + "] on table [" - + tableName.toLowerCase(Locale.ENGLISH) + "] that does not exist in the database"); - } - } else { - // if no annotation, try guessed column names - final Set guessedNames = nameGuesser.guessColumn(fieldName); - for (String guessedColumnName : guessedNames) { - columnName = getIgnoreCase(columnsList, guessedColumnName); - if (columnName != null) { - break; - } - } - if (columnName == null) { - throw new PersistException("Field [" + fieldName + "] from class [" + objectClass.getName() - + "] does not specify a column name through a Column annotation and no guessed column names " - + guessedNames + " exist in the database. If this field is not supposed to be associated " - + "with the database, please annotate it with @NoColumn"); - } - } - - return columnName; - } - - // ---------- helpers ---------- - - /** - * Returns the first entry from the provided collection that matches the - * provided string ignoring case during the comparison. - */ - private static String getIgnoreCase(final Collection collection, final String str) { - String ret = null; - for (String s : collection) { - if (s.equalsIgnoreCase(str)) { - ret = s; - break; - } - } - return ret; - } - - private static String[] toArray(final List list) { - String[] array = new String[list.size()]; - for (int i = 0; i < list.size(); i++) { - array[i] = list.get(i); - } - return array; - } - - private static String[] toArray(final Set set) { - final String[] array = new String[set.size()]; - int i = 0; - for (String s : set) { - array[i] = s; - i++; - } - return array; - } - - private static String join(final String[] list, final String suffix, final String separator) { - final StringBuffer buf = new StringBuffer(); - for (String obj : list) { - buf.append(obj.toString()).append(suffix).append(separator); - } - if (buf.length() > 0 && separator.length() > 0) { - buf.delete(buf.length() - separator.length(), buf.length()); - } - return buf.toString(); - } - - private static String multiply(final String str, final int times, final String separator) { - final StringBuffer buf = new StringBuffer(); - for (int i = 0; i < times; i++) { - buf.append(str).append(separator); - } - if (separator.length() > 0) { - buf.delete(buf.length() - separator.length(), buf.length()); - } - return buf.toString(); - } + private final Class objectClass; + private final Table tableAnnotation; + private final String tableName; + + private final String[] fields; // list of fields which have getters and setters + private final Map annotationsMap; // maps field names to annotations + private final Map gettersMap; // maps field names to getters + private final Map settersMap; // maps field names to setters + + private final boolean supportsGetGeneratedKeys; + private final boolean supportsBatchUpdates; + + private final Map columnsMap = new LinkedHashMap(32); // maps table columns to property names + private final String[] columns; + private final String[] primaryKeys; + private final String[] notPrimaryKeys; + private final String[] autoGeneratedColumns; + private final String[] notAutoGeneratedColumns; + + private final String selectSql; + private final String selectAllSql; + private final String insertSql; + private final String updateSql; + private final String deleteSql; + + public TableMapping(final DatabaseMetaData metaData, final Class objectClass, final NameGuesser nameGuesser) + throws SQLException { + + ResultSet resultSet = null; + + // object class + this.objectClass = objectClass; + + // database support for auto increment keys + supportsGetGeneratedKeys = metaData.supportsGetGeneratedKeys(); + + // database support for batch updates + supportsBatchUpdates = metaData.supportsBatchUpdates(); + + // database name + final String databaseProductName = metaData.getDatabaseProductName(); + + // table annotation + tableAnnotation = (Table) objectClass.getAnnotation(Table.class); + + // schema pattern + String schemaPattern = null; + if (databaseProductName.equalsIgnoreCase("Oracle")) { + schemaPattern = "%"; // oracle expects a pattern such as "%" to work + } + + // table name and annotation + tableName = getTableName(metaData, schemaPattern, objectClass, nameGuesser); + + // all column names and types (from db) + + final List columnsList = new ArrayList(32); + resultSet = metaData.getColumns(null, schemaPattern, tableName, "%"); + while (resultSet.next()) { + final String columnName = resultSet.getString(4); + columnsList.add(columnName); + } + resultSet.close(); + columns = toArray(columnsList); + + // all primary keys (from db) + + final List primaryKeysList = new ArrayList(32); + resultSet = metaData.getPrimaryKeys(null, schemaPattern, tableName); + while (resultSet.next()) { + final String columnName = resultSet.getString(4); + primaryKeysList.add(columnName); + } + resultSet.close(); + primaryKeys = toArray(primaryKeysList); + + // not primary keys + + final List notPrimaryKeysList = new ArrayList(32); + for (String columnName : columns) { + if (!primaryKeysList.contains(columnName)) { + notPrimaryKeysList.add(columnName); + } + } + notPrimaryKeys = toArray(notPrimaryKeysList); + + // map field names to annotations, getters and setters + + final Map[] fieldsMaps = getFieldsMaps(objectClass); + annotationsMap = fieldsMaps[0]; + gettersMap = fieldsMaps[1]; + settersMap = fieldsMaps[2]; + fields = toArray(gettersMap.keySet()); + + // map column names to field names; create list of auto-increment columns + // columnsMap use keys in lower case + + // the actual autoGeneratedColumns list should have columns in the database order + final Set autoGeneratedColumnsTemp = new HashSet(); + for (String fieldName : fields) { + final String columnName = getColumnName(objectClass, nameGuesser, annotationsMap, columnsList, tableName, + fieldName); + columnsMap.put(columnName.toLowerCase(Locale.ENGLISH), fieldName); + final Column annotation = annotationsMap.get(fieldName); + if (annotation != null && annotation.autoGenerated()) { + autoGeneratedColumnsTemp.add(columnName); + } + } + + // auto-increment and not-auto-increment columns, in the database order + + final List notAutoGeneratedColumnsList = new ArrayList(); + final List autoGeneratedColumnsList = new ArrayList(); + for (String columnName : columns) { + if (autoGeneratedColumnsTemp.contains(columnName)) { + autoGeneratedColumnsList.add(columnName); + } else { + notAutoGeneratedColumnsList.add(columnName); + } + } + notAutoGeneratedColumns = toArray(notAutoGeneratedColumnsList); + autoGeneratedColumns = toArray(autoGeneratedColumnsList); + + // assemble sql blocks to be used by crud sql statements + + final String allColumns = join(columns, "", ","); + final String noAutoColumns = join(notAutoGeneratedColumns, "", ","); + final String allPlaceholders = multiply("?", columns.length, ","); + final String noAutoPlaceholders = multiply("?", notAutoGeneratedColumns.length, ","); + final String where = join(primaryKeys, "=?", " and "); + final String updateSet = join(notPrimaryKeys, "=?", ","); + + // assemble crud sql statements + + selectSql = "select " + allColumns + " from " + tableName + " where " + where; + selectAllSql = "select " + allColumns + " from " + tableName; + + if (autoGeneratedColumns.length == 0) { + insertSql = "insert into " + tableName + "(" + allColumns + ")values(" + allPlaceholders + ")"; + } else { + insertSql = "insert into " + tableName + "(" + noAutoColumns + ")values(" + noAutoPlaceholders + ")"; + } + + updateSql = "update " + tableName + " set " + updateSet + " where " + where; + deleteSql = "delete from " + tableName + " where " + where; + + } + + // ---------- getters and setters ---------- + + public boolean supportsGetGeneratedKeys() { + return supportsGetGeneratedKeys; + } + + public boolean supportsBatchUpdates() { + return supportsBatchUpdates; + } + + public Class getObjectClass() { + return objectClass; + } + + public String getTableName() { + return tableName; + } + + public Table getTableAnnotation() { + return tableAnnotation; + } + + public String[] getColumns() { + return columns; + } + + public Map getColumnsMap() { + return columnsMap; + } + + public String[] getPrimaryKeys() { + return primaryKeys; + } + + public String[] getNotPrimaryKeys() { + return notPrimaryKeys; + } + + public String[] getAutoGeneratedColumns() { + return autoGeneratedColumns; + } + + public String[] getNotAutoGeneratedColumns() { + return notAutoGeneratedColumns; + } + + public String[] getFields() { + return fields; + } + + public Map getAnnotationsMap() { + return annotationsMap; + } + + public Map getGettersMap() { + return gettersMap; + } + + public Map getSettersMap() { + return settersMap; + } + + public Method getGetterForColumn(final String columnName) { + final String fieldName = columnsMap.get(columnName.toLowerCase(Locale.ENGLISH)); + return gettersMap.get(fieldName); + } + + public Method getSetterForColumn(final String columnName) { + final String fieldName = columnsMap.get(columnName.toLowerCase(Locale.ENGLISH)); + return settersMap.get(fieldName); + } + + public String getSelectSql() { + return selectSql; + } + + public String getSelectAllSql() { + return selectAllSql; + } + + public String getInsertSql() { + return insertSql; + } + + public String getUpdateSql() { + return updateSql; + } + + public String getDeleteSql() { + return deleteSql; + } + + // ---------- utility methods ---------- + + private static String getTableName(final DatabaseMetaData metaData, final String schema, final Class objectClass, + final NameGuesser nameGuesser) throws SQLException { + + String name = null; + + final Table tableAnnotation = (Table) objectClass + .getAnnotation(Table.class); + + if (tableAnnotation != null && !tableAnnotation.name().equals("")) { + // if there's a Table annotation, use it + name = checkTableName(metaData, schema, tableAnnotation.name()); + + // test if the specified table name actually exists + if (name == null) { + throw new PersistException("Class [" + objectClass.getName() + "] specifies table [" + + tableAnnotation.name() + "] that does not exist in the database"); + } + } else { + // if no annotation, try guessed table names + final String className = objectClass.getSimpleName().substring(0, 1).toLowerCase() + + objectClass.getSimpleName().substring(1); + final Set guessedNames = nameGuesser.guessColumn(className); + for (String guessedTableName : guessedNames) { + name = checkTableName(metaData, schema, guessedTableName); + if (name != null) { + break; + } + } + if (name == null) { + throw new PersistException("Class [" + objectClass.getName() + + "] does not specify a table name through a Table annotation and no guessed table names " + + guessedNames + " exist in the database"); + } + } + + return name; + } + + /** + * Check if the given name corresponds to a table in the database and + * returns the corresponding name with the capitalization returned by the + * database metadata + */ + private static String checkTableName(final DatabaseMetaData metaData, final String schema, final String tableName) + throws SQLException { + + ResultSet resultSet; + String ret = null; + + // try name in upper case -- should work in most databases + resultSet = metaData.getTables(null, schema, tableName.toUpperCase(Locale.ENGLISH), null); + if (resultSet.next()) { + ret = tableName.toUpperCase(Locale.ENGLISH); + } + resultSet.close(); + + if (ret == null) { + // try name in lower case + resultSet = metaData.getTables(null, schema, tableName.toLowerCase(Locale.ENGLISH), null); + if (resultSet.next()) { + ret = tableName.toLowerCase(Locale.ENGLISH); + } + resultSet.close(); + } + + if (ret == null) { + // last resort: compare with all table names in the schema + // (may be very expensive in databases such as oracle) + // this may end up being used in databases that allow case sensitive names (such as postgresql) + resultSet = metaData.getTables(null, schema, "%", null); + while (resultSet.next()) { + final String dbTableName = resultSet.getString(3); + if (tableName.equalsIgnoreCase(dbTableName)) { + ret = dbTableName; + break; + } + } + resultSet.close(); + } + + return ret; + } + + private static String getColumnName(final Class objectClass, final NameGuesser nameGuesser, + final Map annotationsMap, final List columnsList, + final String tableName, final String fieldName) throws SQLException { + + String columnName = null; + + final Column annotation = annotationsMap.get(fieldName); + if (annotation != null && !annotation.name().equals("")) { + // if there's an annotation, use it + columnName = getIgnoreCase(columnsList, annotation.name()); + + // check if the specified column actually exists in the table + if (columnName == null) { + throw new PersistException("Field [" + fieldName + "] from class [" + objectClass.getName() + + "] specifies column [" + annotation.name() + "] on table [" + + tableName.toLowerCase(Locale.ENGLISH) + "] that does not exist in the database"); + } + } else { + // if no annotation, try guessed column names + final Set guessedNames = nameGuesser.guessColumn(fieldName); + for (String guessedColumnName : guessedNames) { + columnName = getIgnoreCase(columnsList, guessedColumnName); + if (columnName != null) { + break; + } + } + if (columnName == null) { + throw new PersistException("Field [" + fieldName + "] from class [" + objectClass.getName() + + "] does not specify a column name through a Column annotation and no guessed column names " + + guessedNames + " exist in the database. If this field is not supposed to be associated " + + "with the database, please annotate it with @NoColumn"); + } + } + + return columnName; + } + + // ---------- helpers ---------- + + /** + * Returns the first entry from the provided collection that matches the + * provided string ignoring case during the comparison. + */ + private static String getIgnoreCase(final Collection collection, final String str) { + String ret = null; + for (String s : collection) { + if (s.equalsIgnoreCase(str)) { + ret = s; + break; + } + } + return ret; + } + + private static String[] toArray(final List list) { + String[] array = new String[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + private static String[] toArray(final Set set) { + final String[] array = new String[set.size()]; + int i = 0; + for (String s : set) { + array[i] = s; + i++; + } + return array; + } + + private static String join(final String[] list, final String suffix, final String separator) { + final StringBuffer buf = new StringBuffer(); + for (String obj : list) { + buf.append(obj.toString()).append(suffix).append(separator); + } + if (buf.length() > 0 && separator.length() > 0) { + buf.delete(buf.length() - separator.length(), buf.length()); + } + return buf.toString(); + } + + private static String multiply(final String str, final int times, final String separator) { + final StringBuffer buf = new StringBuffer(); + for (int i = 0; i < times; i++) { + buf.append(str).append(separator); + } + if (separator.length() > 0) { + buf.delete(buf.length() - separator.length(), buf.length()); + } + return buf.toString(); + } + + UpdateInfo getCurrentUpdateInfo(Object object) { + + UpdateInfo info = new UpdateInfo(); + info.sql = ""; + Object original = ((PersistableObject) object).originalValue; + try { + List changedFields = new ArrayList(); + for (String key : gettersMap.keySet()) { + + Object leftValue = gettersMap.get(key).invoke(object); + Object rightValue = gettersMap.get(key).invoke(original); + + if (leftValue != null && !leftValue.equals(rightValue) || rightValue != null && !rightValue.equals(leftValue)) { + changedFields.add(key); + } + } + + Log.debug(Log.ENGINE, "CHANGED FIELDS: " + changedFields); + + // at this point we can use mapping.getColumnsMap().keySet() + List changedColumns = new ArrayList(); + for (String col : columnsMap.keySet()) { + Log.debug(Log.ENGINE, col + " = " + columnsMap.get(col)); + if (changedFields.contains(columnsMap.get(col))) { + changedColumns.add(col); + } + } + + + // Need to be in the same order as notAutoGeneratedColumns array + List sortedChangedColumns = new ArrayList(); + for (String col : notAutoGeneratedColumns) { + boolean found = false; + for (String ccol : changedColumns) { + if (col.equalsIgnoreCase(ccol)) { + found = true; + break; + } + } + if (found) { + sortedChangedColumns.add(col); + } + } + + String[] cols = toArray(sortedChangedColumns); + final String where = join(primaryKeys, "=?", ","); + final String updateSet = join(cols, "=?", ","); + info.sql = "update " + tableName + " set " + updateSet + " where " + where; + Collections.addAll(sortedChangedColumns, primaryKeys); + + info.columns = toArray(sortedChangedColumns); + + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + return info; + } } diff --git a/src/main/net/sf/persist/UpdateInfo.java b/src/main/net/sf/persist/UpdateInfo.java new file mode 100644 index 0000000..3253d43 --- /dev/null +++ b/src/main/net/sf/persist/UpdateInfo.java @@ -0,0 +1,12 @@ +package net.sf.persist; + +/** + * Comments for UpdateInfo go here. + * + * @author Dan Howard + * @since Dec 6, 2008 5:22:29 PM + */ +class UpdateInfo { + String sql; + String[] columns; +} diff --git a/src/main/net/sf/persist/annotations/Column.java b/src/main/net/sf/persist/annotations/Column.java index 3f5cfd9..25ffaf1 100644 --- a/src/main/net/sf/persist/annotations/Column.java +++ b/src/main/net/sf/persist/annotations/Column.java @@ -12,20 +12,20 @@ * setter of the field being mapped. */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ElementType.METHOD, ElementType.FIELD}) public @interface Column { - /** - * Name of the column mapped to the field. - */ - String name() default ""; // a @Column annotation can leave name undefined, since it may be only concerned with autoGenerated + /** + * Name of the column mapped to the field. + */ + String name() default ""; // a @Column annotation can leave name undefined, since it may be only concerned with autoGenerated - /** - * If the field is auto-generated in the database (eg. auto increment - * fields). This will hint the engine to avoid using the field in - * insert/update automatic operations, and to let it know which fields must - * be updated if updateAutoGeneratedKeys is set to true. - */ - boolean autoGenerated() default false; + /** + * If the field is auto-generated in the database (eg. auto increment + * fields). This will hint the engine to avoid using the field in + * insert/update automatic operations, and to let it know which fields must + * be updated if updateAutoGeneratedKeys is set to true. + */ + boolean autoGenerated() default false; } diff --git a/src/main/net/sf/persist/annotations/NoColumn.java b/src/main/net/sf/persist/annotations/NoColumn.java index a301662..1bb035a 100644 --- a/src/main/net/sf/persist/annotations/NoColumn.java +++ b/src/main/net/sf/persist/annotations/NoColumn.java @@ -12,7 +12,7 @@ * getter or a setter associated with a field. */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) +@Target({ElementType.METHOD, ElementType.FIELD}) public @interface NoColumn { }