diff --git a/src/main/net/sf/persist/AnnotationTableMapping.java b/src/main/net/sf/persist/AnnotationTableMapping.java index faa07ee..c5b7d56 100644 --- a/src/main/net/sf/persist/AnnotationTableMapping.java +++ b/src/main/net/sf/persist/AnnotationTableMapping.java @@ -9,8 +9,12 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; import java.util.Set; +import net.sf.persist.writer.Writer; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,6 +95,7 @@ public AnnotationTableMapping(final Class objectClass) { if (annotation != null && annotation.autoGenerated()) { autoGeneratedColumnsTemp.add(columnName); } + } if (primaryKeysList.isEmpty()) { @@ -194,16 +199,40 @@ public Map getSettersMap() { return settersMap; } + public Optional getFieldName(final String columnName) { + Optional fieldName = Optional.ofNullable(columnsMap.get(columnName.toLowerCase(Locale.ENGLISH))); + if (!fieldName.isPresent()) { + ENGINE_LOG.warn("Column name [" + columnName + "] has no corresponding field."); + } + return fieldName; + } + @Override public Method getGetterForColumn(final String columnName) { - final String fieldName = columnsMap.get(columnName.toLowerCase(Locale.ENGLISH)); - return gettersMap.get(fieldName); + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional getterForFieldName = Optional.ofNullable(gettersMap.get(fieldName.get())); + if (!getterForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find getter for columnn with field name [" + fieldName + "]"); + } + return getterForFieldName.get(); + } + return null; } @Override public Method getSetterForColumn(final String columnName) { - final String fieldName = columnsMap.get(columnName.toLowerCase(Locale.ENGLISH)); - return settersMap.get(fieldName); + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional setterForFieldName = Optional.ofNullable(settersMap.get(fieldName.get())); + if (!setterForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find setter for columnn with field name [" + fieldName + "]"); + } + return setterForFieldName.get(); + } + return null; } public String getSelectSql() { @@ -226,6 +255,51 @@ public String getDeleteSql() { return deleteSql; } + @Override + public Class getOptionalSubType(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional> optSubTypeForFieldName = + Optional.ofNullable(annotationsMap.get(fieldName.get()).optionalSubType()); + if (!optSubTypeForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find optional subtype for columnn with field name [" + fieldName + "]"); + } + return optSubTypeForFieldName.get(); + } + return null; + } + + @Override + public Class getSerializationType(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional> serializationTypeForFieldName = + Optional.ofNullable(annotationsMap.get(fieldName.get()).serializeAs()); + if (!serializationTypeForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find serialization type for columnn with field name [" + fieldName + "]"); + } + return serializationTypeForFieldName.get(); + } + return null; + } + + @Override + public Class getWriterClass(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional> writerClassForFieldName = + Optional.ofNullable(annotationsMap.get(fieldName.get()).writerClass()); + if (!writerClassForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find writer class for columnn with field name [" + fieldName + "]"); + } + return writerClassForFieldName.get(); + } + return null; + } + // ---------- helpers ---------- private static String[] toArray(final List list) { diff --git a/src/main/net/sf/persist/Mapping.java b/src/main/net/sf/persist/Mapping.java index 2b97390..7f19f2b 100644 --- a/src/main/net/sf/persist/Mapping.java +++ b/src/main/net/sf/persist/Mapping.java @@ -7,12 +7,20 @@ import java.util.HashMap; import java.util.Map; +import net.sf.persist.writer.Writer; + public abstract class Mapping { public abstract Method getGetterForColumn(String columnName); public abstract Method getSetterForColumn(String columnName); + public abstract Class getOptionalSubType(String columnName); + + public abstract Class getSerializationType(String columnName); + + public abstract Class getWriterClass(String columnName); + // ---------- utility methods ---------- /** @@ -49,7 +57,7 @@ protected static Map[] getFieldsMaps(final Class objectClass) { // create map with all getters and setters - final Map allMethods = new HashMap(); + final Map allMethods = new HashMap<>(); for (Method method : methods) { final String name = method.getName(); @@ -86,9 +94,10 @@ protected static Map[] getFieldsMaps(final Class objectClass) { // 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(); + final Map annotationsMap = new HashMap<>(); + final Map gettersMap = new HashMap<>(); + final Map settersMap = new HashMap<>(); + final Map> optionalSubTypeMap = new HashMap<>(); for (String suffix : allMethods.keySet()) { diff --git a/src/main/net/sf/persist/NoTableMapping.java b/src/main/net/sf/persist/NoTableMapping.java index fc73599..0eef7e2 100644 --- a/src/main/net/sf/persist/NoTableMapping.java +++ b/src/main/net/sf/persist/NoTableMapping.java @@ -4,9 +4,17 @@ import java.lang.reflect.Method; import java.util.HashMap; +import java.util.Locale; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; import java.util.Set; +import net.sf.persist.writer.Writer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Represents the mapping of columns to getters and setters of a POJO. *

@@ -17,6 +25,8 @@ */ public class NoTableMapping extends Mapping { + private static final Logger ENGINE_LOG = LoggerFactory.getLogger("persist.engine"); + // POJO class private final Class objectClass; @@ -29,6 +39,8 @@ public class NoTableMapping extends Mapping { // map possible column names to field names private final Map columnsMap; + private final Map annotationsMap; + public NoTableMapping(Class objectClass, NameGuesser nameGuesser) { checkAnnotation(objectClass); @@ -37,7 +49,7 @@ public NoTableMapping(Class objectClass, NameGuesser nameGuesser) { // get the list of annotations, getters and setters Map[] fieldsMaps = Mapping.getFieldsMaps(objectClass); - final Map annotationsMap = fieldsMaps[0]; + annotationsMap = fieldsMaps[0]; gettersMap = fieldsMaps[1]; settersMap = fieldsMaps[2]; @@ -91,16 +103,10 @@ public NoTableMapping(Class objectClass, NameGuesser nameGuesser) { } - /** - * 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."); + public Optional getFieldName(final String columnName) { + Optional fieldName = Optional.ofNullable(columnsMap.get(columnName.toLowerCase(Locale.ENGLISH))); + if (!fieldName.isPresent()) { + ENGINE_LOG.warn("Column name [" + columnName + "] has no corresponding field."); } return fieldName; } @@ -112,9 +118,17 @@ public String getFieldNameForColumn(String columnName) { * @see Mapping */ @Override - public Method getSetterForColumn(String columnName) { - String fieldName = getFieldNameForColumn(columnName); - return settersMap.get(fieldName); + public Method getSetterForColumn(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional setterForFieldName = Optional.ofNullable(settersMap.get(fieldName.get())); + if (!setterForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find setter for columnn with field name [" + fieldName + "]"); + } + return setterForFieldName.get(); + } + return null; } /** @@ -124,11 +138,65 @@ public Method getSetterForColumn(String columnName) { * @see Mapping */ @Override - public Method getGetterForColumn(String columnName) { - String fieldName = getFieldNameForColumn(columnName); - return gettersMap.get(fieldName); + public Method getGetterForColumn(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional getterForFieldName = Optional.ofNullable(gettersMap.get(fieldName.get())); + if (!getterForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find getter for columnn with field name [" + fieldName + "]"); + } + return getterForFieldName.get(); + } + return null; + } + + @Override + public Class getOptionalSubType(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional> optSubTypeForFieldName = + Optional.ofNullable(annotationsMap.get(fieldName.get()).optionalSubType()); + if (!optSubTypeForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find optional subtype for columnn with field name [" + fieldName + "]"); + } + return optSubTypeForFieldName.get(); + } + return null; + } + + @Override + public Class getSerializationType(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional> serializationTypeForFieldName = + Optional.ofNullable(annotationsMap.get(fieldName.get()).serializeAs()); + if (!serializationTypeForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find serialization type for columnn with field name [" + fieldName + "]"); + } + return serializationTypeForFieldName.get(); + } + return null; } + @Override + public Class getWriterClass(final String columnName) { + final Optional fieldName = getFieldName(columnName); + if (fieldName.isPresent()) { + final Optional> writerClassForFieldName = + Optional.ofNullable(annotationsMap.get(fieldName.get()).writerClass()); + if (!writerClassForFieldName.isPresent()) { + throw new NoSuchElementException( + "Could not find writer class for columnn with field name [" + fieldName + "]"); + } + return writerClassForFieldName.get(); + } + return null; + } + + /** * Checks if a given column name conflicts with an existing name in the * columns map. diff --git a/src/main/net/sf/persist/Persist.java b/src/main/net/sf/persist/Persist.java index 0466432..e8bd05a 100644 --- a/src/main/net/sf/persist/Persist.java +++ b/src/main/net/sf/persist/Persist.java @@ -18,9 +18,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import net.sf.persist.writer.VoidWriter; +import net.sf.persist.writer.Writer; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,8 +72,8 @@ public final class Persist { private PreparedStatement lastPreparedStatement = null; private boolean closePreparedStatementsAfterRead = true; - private static ConcurrentMap> mappingCaches = new ConcurrentHashMap>(); - private static ConcurrentMap nameGuessers = new ConcurrentHashMap(); + private static ConcurrentMap> mappingCaches = new ConcurrentHashMap<>(); + private static ConcurrentMap nameGuessers = new ConcurrentHashMap<>(); private static final String DEFAULT_CACHE = "default cache"; @@ -142,7 +146,7 @@ public Persist(String cacheName, Connection connection, ObjectFactory objectFact } public static void flushMappings() { - mappingCaches = new ConcurrentHashMap>(); + mappingCaches = new ConcurrentHashMap<>(); } // ---------- name guesser ---------- @@ -211,7 +215,7 @@ public Mapping getMapping(final Class objectClass) { cacheName = DEFAULT_CACHE; } - ConcurrentMap mappingCache = new ConcurrentHashMap(); + ConcurrentMap mappingCache = new ConcurrentHashMap<>(); ConcurrentMap previousMappingCache = mappingCaches.putIfAbsent(cacheName, mappingCache); if (previousMappingCache != null) { mappingCache = previousMappingCache; @@ -231,6 +235,7 @@ public Mapping getMapping(final Class objectClass) { } return mappingCache.get(objectClass); + } /** @@ -476,18 +481,206 @@ public boolean isClosePreparedStatementsAfterRead() { * 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++) { + + Object parameter = parameters[i - 1]; + + if (parameter != null && parameter.getClass() == Optional.class) { + Optional instanceOfParameter = (Optional) parameter; + parameter = instanceOfParameter.orElse(null); + } + + 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 (PARAMETERS_LOG.isDebugEnabled()) { + PARAMETERS_LOG.debug("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 : "" + 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 { + if (reader instanceof SizeAware) { + size = ((SizeAware) reader).size(); + } else { + 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 { + if (inputStream instanceof SizeAware) { + size = ((SizeAware) inputStream).size(); + } else { + 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.time.Instant.class) { + final java.time.Instant instant = (java.time.Instant) parameter; + stmt.setTimestamp(i, new java.sql.Timestamp(instant.toEpochMilli())); + } 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 if (parameter instanceof java.lang.Enum) { + stmt.setString(i, ((Enum) parameter).name()); + } else { + // last resort; this should cover all database-specific + // object types + stmt.setObject(i, parameter); + } + + if (PARAMETERS_LOG.isDebugEnabled()) { + PARAMETERS_LOG.debug("PreparedStatement [" + stmt + "] Parameter [" + i + "] type [" + + type.getSimpleName() + "] set to [" + Log.objectToString(parameter) + "]"); + } + + } catch (SQLException e) { + throw new RuntimeSQLException(e); + } + } + } + + + public static void setParameters(final PreparedStatement stmt, final Class[] serializationTypes, + final Class[] writerClasses, 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]; + //parameter, serializationType, and writerClass arrays are all ordered corresponding to the same columns + Object parameter = parameters[i - 1]; + Class serializationType = serializationTypes[i - 1]; + Class writerClass = writerClasses[i - 1]; + + if (parameter != null && parameter.getClass() == Optional.class) { + Optional instanceOfParameter = (Optional) parameter; + parameter = instanceOfParameter.orElse(null); + } if (parameter == null) { @@ -513,17 +706,34 @@ public static void setParameters(final PreparedStatement stmt, final Object[] pa 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); + + "] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "] " + e.getMessage(), e); } if (PARAMETERS_LOG.isDebugEnabled()) { PARAMETERS_LOG.debug("Parameter [" + i + "] from PreparedStatement [" + stmt - + "] set to [null] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "]"); + + "] set to [null] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "]"); } continue; } + if ((serializationType != Void.class) && (writerClass == VoidWriter.class)) { + throw new PersistException( + "Serialization type set as [" + serializationType.getName() + + "] but writerClass was VoidWriter.class"); + } else if ((serializationType == Void.class) && (writerClass != VoidWriter.class)) { + PARAMETERS_LOG.error("No serialization type was set but writerClass was not VoidWriter.class"); + } + + if ((serializationType != Void.class) && (writerClass != VoidWriter.class)) { + try { + parameter = writerClass.newInstance().readFromStrongType(parameter); + } catch (InstantiationException | IllegalAccessException e) { + throw new PersistException( + "Failed to create new instance of writer class [" + writerClass.getName() + "]"); + } + } + try { final Class type = parameter.getClass(); @@ -630,7 +840,7 @@ public static void setParameters(final PreparedStatement stmt, final Object[] pa } else if (type == java.sql.Timestamp.class) { stmt.setTimestamp(i, (java.sql.Timestamp) parameter); } else if (parameter instanceof java.lang.Enum) { - stmt.setString(i, ((Enum)parameter).name()); + stmt.setString(i, ((Enum) parameter).name()); } else { // last resort; this should cover all database-specific // object types @@ -639,7 +849,7 @@ public static void setParameters(final PreparedStatement stmt, final Object[] pa if (PARAMETERS_LOG.isDebugEnabled()) { PARAMETERS_LOG.debug("PreparedStatement [" + stmt + "] Parameter [" + i + "] type [" - + type.getSimpleName() + "] set to [" + Log.objectToString(parameter) + "]"); + + type.getSimpleName() + "] set to [" + Log.objectToString(parameter) + "]"); } } catch (SQLException e) { @@ -712,7 +922,7 @@ private static boolean isNativeType(final Class type) { * for a null value from the {@link java.sql.ResultSet}. * * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be - * processed) + * processed) * @param column column index in the result set (starting with 1) * @param type {@link java.lang.Class} of the object to be returned * @@ -725,7 +935,6 @@ public static T getValueFromResultSet(final ResultSet resultSet, final int c Object value; try { - if (type == boolean.class) { value = resultSet.getBoolean(column); } else if (type == Boolean.class) { @@ -817,6 +1026,15 @@ public static T getValueFromResultSet(final ResultSet resultSet, final int c return (T)value; } + public static Optional getValueFromResultSetOptional(final ResultSet resultSet, final int column, + final Class optionalSubType) { + if (getValueFromResultSet(resultSet, column, optionalSubType) == null) { + return Optional.empty(); + } else { + return Optional.of(getValueFromResultSet(resultSet, column, optionalSubType)); + } + } + /** * Reads a column from the current row in the provided * {@link java.sql.ResultSet} and return a value correspondent to the SQL @@ -997,6 +1215,26 @@ private static Object[] getParametersFromObject(final Object object, final Strin return parameters; } + private static Class[] getSerializationTypesFromObject(final Object object, final String[] columns, + final AnnotationTableMapping mapping) { + + Class[] serializationTypes = new Class[columns.length]; + for (int i = 0; i < columns.length; i++) { + serializationTypes[i] = mapping.getSerializationType(columns[i]); + } + return serializationTypes; + } + + private static Class[] getWriterClassesFromObject(final Object object, final String[] columns, + final AnnotationTableMapping mapping) { + + Class[] writerClasses = new Class[columns.length]; + for (int i = 0; i < columns.length; i++) { + writerClasses[i] = mapping.getWriterClass(columns[i]); + } + return writerClasses; + } + /** * Reads a row from the provided {@link java.sql.ResultSet} and converts it * to an object instance of the given class. @@ -1032,7 +1270,6 @@ public T loadObject(final Class objectClass, final ResultSet resultSet) t // for beans else { - final Mapping mapping = getMapping(objectClass); try { @@ -1046,15 +1283,61 @@ public T loadObject(final Class objectClass, final ResultSet resultSet) t } for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { + final String columnName = resultSetMetaData.getColumnLabel(i).toLowerCase(); final Method setter = mapping.getSetterForColumn(columnName); + if (setter == null) { PARAMETERS_LOG.warn("Column [" + columnName + "] from result set does not have a mapping to a field in [" + objectClass.getName() + "]"); } else { - final Class type = setter.getParameterTypes()[0]; - final Object value = getValueFromResultSet(resultSet, i, type); + Class type = setter.getParameterTypes()[0]; + Class optionalSubType = mapping.getOptionalSubType(columnName); + final Class serializationType = mapping.getSerializationType(columnName); + final Class writerClass = mapping.getWriterClass(columnName); + + if ((type == Optional.class) && (optionalSubType == Void.class)) { + throw new PersistException( + "Column [" + columnName + "] was Optional but optionalSubType was Void.class"); + } else if ((type != Optional.class) && (optionalSubType != Void.class)) { + PARAMETERS_LOG.error("Column type was not Optional but optionalSubType was not Void.class"); + } + + if ((serializationType != Void.class) && (writerClass == VoidWriter.class)) { + throw new PersistException( + "Column [" + columnName + + "] had a serialization type set but writerClass was VoidWriter.class"); + } else if ((serializationType == Void.class) && (writerClass != VoidWriter.class)) { + PARAMETERS_LOG.error("No serialization type was set but writerClass was not VoidWriter.class"); + } + + Object value; + + // If there is a properly-implemented serialization type, then Persist should get the value + // using the serialization type, then use the writer class to create an object of the desired type + if ((serializationType != Void.class) && (writerClass != VoidWriter.class)) { + Object serializedValue; + if (type == Optional.class) { + optionalSubType = serializationType; + serializedValue = getValueFromResultSetOptional(resultSet, i, optionalSubType); + } else { + type = serializationType; + serializedValue = getValueFromResultSet(resultSet, i, type); + } + try { + value = writerClass.newInstance().writeToStrongType(serializedValue); + } catch (InstantiationException | IllegalAccessException e) { + throw new PersistException( + "Failed to create new instance of writer class for column [" + columnName + "]"); + } + } else { + if (type == Optional.class) { + value = getValueFromResultSetOptional(resultSet, i, optionalSubType); + } else { + value = getValueFromResultSet(resultSet, i, type); + } + } try { setter.invoke(ret, value); @@ -1085,7 +1368,7 @@ public T loadObject(final Class objectClass, final ResultSet resultSet) t */ public static Map loadMap(final ResultSet resultSet) throws SQLException { - final Map ret = new LinkedHashMap(); + final Map ret = new LinkedHashMap<>(); final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) { @@ -1114,7 +1397,19 @@ public void setAutoGeneratedKeys(final Object object, final Result result) { 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); + + Object key; + if (Void.class.equals((mapping.getSerializationType(columnName)))) { + key = result.getGeneratedKeys().get(i); + } else { + final Object serializedKey = result.getGeneratedKeys().get(i); + try { + key = mapping.getWriterClass(columnName).newInstance().writeToStrongType(serializedKey); + } catch (InstantiationException | IllegalAccessException e) { + throw new PersistException( + "Failed to create new instance of writer class for column [" + columnName + "]"); + } + } try { setter.invoke(object, key); } catch (Exception e) { @@ -1146,7 +1441,7 @@ public void setAutoGeneratedKeys(final Object object, final Result result) { */ @SuppressWarnings("unchecked") public Result executeUpdate(final Class objectClass, final String sql, final String[] autoGeneratedKeys, - final Object...parameters) { + final Class[] serializationTypes, final Class[] writerClasses, final Object... parameters) { long begin = 0; if (PROFILING_LOG.isDebugEnabled()) { @@ -1156,7 +1451,7 @@ public Result executeUpdate(final Class objectClass, final String sql, final Str final PreparedStatement stmt = getPreparedStatement(sql, autoGeneratedKeys); try { - setParameters(stmt, parameters); + setParameters(stmt, serializationTypes, writerClasses, parameters); int rowsModified = 0; try { @@ -1204,6 +1499,67 @@ public Result executeUpdate(final Class objectClass, final String sql, final Str } } + @SuppressWarnings("unchecked") + public Result executeUpdate(final Class objectClass, final String sql, final String[] autoGeneratedKeys, + final Object... parameters) { + + long begin = 0; + if (PROFILING_LOG.isDebugEnabled()) { + 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 (String autoGeneratedKey : autoGeneratedKeys) { + resultSet.next(); + + // get the auto-generated key using the ResultSet.get method + // that matches + // the bean setter parameter type + final Method setter = mapping.getSetterForColumn(autoGeneratedKey); + 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 (PROFILING_LOG.isDebugEnabled()) { + final long end = System.currentTimeMillis(); + PROFILING_LOG.debug("executeUpdate in [{}ms] for sql [{}]", (end - begin), sql); + } + + return result; + } finally { + closePreparedStatement(stmt); + } + } + /** * Executes an update and returns the number of rows modified. *

@@ -1214,14 +1570,15 @@ public Result executeUpdate(final Class objectClass, final String sql, final Str * @param parameters Parameters to be used in the PreparedStatement. * @since 1.0 */ - public int executeUpdate(final String sql, final Object...parameters) { + public int executeUpdate(final String sql, final Class[] serializationTypes, final Class[] writerClasses, + final Object... parameters) { final PreparedStatement stmt = getPreparedStatement(sql); try { int rowsModified = 0; - setParameters(stmt, parameters); + setParameters(stmt, serializationTypes, writerClasses, parameters); rowsModified = stmt.executeUpdate(); return rowsModified; @@ -1233,6 +1590,25 @@ public int executeUpdate(final String sql, final Object...parameters) { } } + public int executeUpdate(final String sql, final Object... parameters) { + + final PreparedStatement stmt = getPreparedStatement(sql); + + try { + int rowsModified = 0; + + setParameters(stmt, parameters); + rowsModified = stmt.executeUpdate(); + + return rowsModified; + } catch (SQLException e) { + throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters " + + Arrays.toString(parameters) + ": " + e.getMessage(), e); + } finally { + closePreparedStatement(stmt); + } + } + // ---------- insert ---------- /** @@ -1246,14 +1622,18 @@ public int insert(final Object object) { final String[] columns = mapping.getNotAutoGeneratedColumns(); final Object[] parameters = getParametersFromObject(object, columns, mapping); + final Class[] serializationTypes = getSerializationTypesFromObject(object, columns, mapping); + final Class[] writerClasses = getWriterClassesFromObject(object, columns, mapping); + int ret = 0; if (updateAutoGeneratedKeys) { - final Result result = executeUpdate(object.getClass(), sql, mapping.getAutoGeneratedColumns(), parameters); + final Result result = executeUpdate(object.getClass(), sql, mapping.getAutoGeneratedColumns(), + serializationTypes, writerClasses, parameters); setAutoGeneratedKeys(object, result); ret = result.getRowsModified(); } else { - ret = executeUpdate(sql, parameters); + ret = executeUpdate(sql, serializationTypes, writerClasses, parameters); } return ret; } @@ -1300,7 +1680,11 @@ public int update(final Object object) { columns[i++] = primaryKey; } final Object[] parameters = getParametersFromObject(object, columns, mapping); - return executeUpdate(sql, parameters); + + final Class[] serializationTypes = getSerializationTypesFromObject(object, columns, mapping); + final Class[] writerClasses = getWriterClassesFromObject(object, columns, mapping); + + return executeUpdate(sql, serializationTypes, writerClasses, parameters); } /** @@ -1340,7 +1724,11 @@ public int delete(final Object object) { final String sql = mapping.getDeleteSql(); final String[] columns = mapping.getPrimaryKeys(); final Object[] parameters = getParametersFromObject(object, columns, mapping); - return executeUpdate(sql, parameters); + + final Class[] serializationTypes = getSerializationTypesFromObject(object, columns, mapping); + final Class[] writerClasses = getWriterClassesFromObject(object, columns, mapping); + + return executeUpdate(sql, serializationTypes, writerClasses, parameters); } /** @@ -1391,7 +1779,7 @@ public T read(final Class objectClass, final String sql) { * * @since 1.0 */ - public T read(final Class objectClass, final String sql, final Object...parameters) { + public T read(final Class objectClass, final String sql, final Object... parameters) { final PreparedStatement stmt = getPreparedStatement(sql); return read(objectClass, stmt, parameters); } @@ -1409,7 +1797,7 @@ public T read(final Class objectClass, final String sql, final Object...p * * @since 1.0 */ - public T read(final Class objectClass, final PreparedStatement statement, final Object...parameters) { + public T read(final Class objectClass, final PreparedStatement statement, final Object... parameters) { try { setParameters(statement, parameters); final ResultSet resultSet = statement.executeQuery(); @@ -1463,7 +1851,7 @@ public T read(final Class objectClass, final ResultSet resultSet) { * * @since 1.0 */ - public T readByPrimaryKey(final Class objectClass, final Object...primaryKeyValues) { + public T readByPrimaryKey(final Class objectClass, final Object... primaryKeyValues) { final AnnotationTableMapping mapping = getTableMapping(objectClass, "readByPrimaryKey()"); final String sql = mapping.getSelectSql(); return read(objectClass, sql, primaryKeyValues); @@ -1487,7 +1875,7 @@ public List readList(final Class objectClass, final ResultSet resultSe begin = System.currentTimeMillis(); } - final List ret = new ArrayList(); + final List ret = new ArrayList<>(); try { while (resultSet.next()) { ret.add(loadObject(objectClass, resultSet)); @@ -1540,7 +1928,7 @@ public List readList(final Class objectClass, final PreparedStatement * * @since 1.0 */ - public List readList(final Class objectClass, final String sql, final Object...parameters) { + public List readList(final Class objectClass, final String sql, final Object... parameters) { final PreparedStatement stmt = getPreparedStatement(sql); try { return readList(objectClass, stmt, parameters); @@ -1588,14 +1976,15 @@ public List readList(final Class objectClass) { * Passed ResultSet and PreparedStatement will be closed when ResultSetIterator is. * @since 1.0 */ - public ResultSetIterator readIterator(final Class objectClass, final ResultSet resultSet, final PreparedStatement preparedStatement) { + public ResultSetIterator readIterator(final Class objectClass, final ResultSet resultSet, + final PreparedStatement preparedStatement) { long begin = 0; if (PROFILING_LOG.isDebugEnabled()) { begin = System.currentTimeMillis(); } - final ResultSetIterator i = new ObjectResultSetIterator(this, objectClass, resultSet, preparedStatement); + final ResultSetIterator i = new ObjectResultSetIterator<>(this, objectClass, resultSet, preparedStatement); if (PROFILING_LOG.isDebugEnabled()) { final long end = System.currentTimeMillis(); @@ -1801,7 +2190,7 @@ public List> readMapList(final ResultSet resultSet) { begin = System.currentTimeMillis(); } - final List> ret = new ArrayList>(); + final List> ret = new ArrayList<>(); try { while (resultSet.next()) { ret.add(loadMap(resultSet)); @@ -2004,7 +2393,8 @@ static class ObjectResultSetIterator implements ResultSetIterator { private final PreparedStatement preparedStatement; private boolean hasNext = false; - ObjectResultSetIterator(final Persist persist, final Class objectClass, final ResultSet resultSet, final PreparedStatement preparedStatement) { + ObjectResultSetIterator(final Persist persist, final Class objectClass, final ResultSet resultSet, + final PreparedStatement preparedStatement) { this.persist = persist; this.objectClass = objectClass; this.resultSet = resultSet; diff --git a/src/main/net/sf/persist/annotations/Column.java b/src/main/net/sf/persist/annotations/Column.java index e65411f..f10d098 100644 --- a/src/main/net/sf/persist/annotations/Column.java +++ b/src/main/net/sf/persist/annotations/Column.java @@ -7,6 +7,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import net.sf.persist.writer.VoidWriter; +import net.sf.persist.writer.Writer; + /** * Defines column mapping for a given field. Must be added to a getter or a * setter of the field being mapped. @@ -33,4 +36,23 @@ * and reading by primary key. */ boolean primaryKey() default false; + + /** + * If the getter/setter use optional values, the sub-type must be explicitly stated due to Java's generic + * subtype handling. + */ + Class optionalSubType() default Void.class; + + /** + * When using a custom, strongly-typed return type, the serialization type for table I/O must be declared. + * For standard serialization types, this does not need to be set, but note that, when reading/writing the + * table, setting this serialization type will override the getter/setter type corresponding to the field. + */ + Class serializeAs() default Void.class; + + /** + * For a custom type, a writer class (extending net.sf.persist.writer.Writer) must be created and referenced + * here, which specifies how to convert a custom type to/from a serialization type. + */ + Class writerClass() default VoidWriter.class; } diff --git a/src/main/net/sf/persist/writer/VoidWriter.java b/src/main/net/sf/persist/writer/VoidWriter.java new file mode 100644 index 0000000..26e832a --- /dev/null +++ b/src/main/net/sf/persist/writer/VoidWriter.java @@ -0,0 +1,15 @@ +package net.sf.persist.writer; + +public class VoidWriter implements Writer { + + @Override + public Object writeToStrongType(Object serializedValue) { + return null; + } + + @Override + public Object readFromStrongType(Object stronglyTypedValue) { + return null; + } + +} diff --git a/src/main/net/sf/persist/writer/Writer.java b/src/main/net/sf/persist/writer/Writer.java new file mode 100644 index 0000000..f4060b1 --- /dev/null +++ b/src/main/net/sf/persist/writer/Writer.java @@ -0,0 +1,8 @@ +package net.sf.persist.writer; + +public interface Writer { + + T writeToStrongType(U serializedValue); + + U readFromStrongType(T stronglyTypedValue); +}