package hirondelle.web4j.database;
import java.util.*;
import java.util.logging.*;
import java.sql.*;
import java.lang.reflect.Constructor;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.model.ConvertParam;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelCtorUtil;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Args;
/**
Construct a Model Object from a <tt>ResultSet</tt>.
<P>Uses all columns. See package level comment for more details.
<P>Can construct either simple Model Objects, passing only building block objects to the constructor,
or may construct limited forms of compound objects as well. See {@link Db} for more details.
*/
class ModelFromRow<E> extends ModelBuilder<E> {
/**
Simple case of a typical Model Object, whose constructor takes only building block values.
@param aClass class literal for the target Model Object.
*/
ModelFromRow(Class<E> aClass){
fClass = aClass;
}
/**
Extended case of a compound Model Object, which takes a single <tt>List</tt> of child objects at the
end of its constructor.
@param aParentClass class literal for the target parent Model Object.
@param aChildClass class literal for the target child Model Object. The child object appears as a <tt>List</tt>
at the end of the parent's constructor.
@param aNumTrailingColsForChildList number of columns at the end of the <tt>ResultSet</tt> that are
used to construct a child object.
*/
ModelFromRow(Class<E> aParentClass, Class<?> aChildClass, int aNumTrailingColsForChildList){
fClass = aParentClass;
fChildClass = aChildClass;
Args.checkForPositive(aNumTrailingColsForChildList);
fNumColsForChildList = aNumTrailingColsForChildList;
}
E build(ResultSet aRow) throws SQLException, ModelCtorException {
E result = null;
if( isSimple() ){
result = buildSimple(aRow);
}
else {
result = buildWithChildList(aRow);
}
return result;
}
// PRIVATE //
private final Class<E> fClass;
private Class<?> fChildClass;
private int fNumColsForChildList;
private ConvertColumn fColumnToObject = BuildImpl.forConvertColumn();
private static final boolean INCLUDE_LAST_CTOR_ARG = true;
private static final boolean EXCLUDE_LAST_CTOR_ARG = false;
private static final int FIRST_COLUMN = 1;
private static final Logger fLogger = Util.getLogger(ModelFromRow.class);
private boolean isSimple(){
return fChildClass == null;
}
private E buildSimple(ResultSet aRow) throws SQLException, ModelCtorException {
Constructor<E> modelCtor = ModelCtorUtil.getConstructor(fClass, getNumColsInResultSet(aRow));
List<Object> ctorArgValues = getCtorArgValues(modelCtor, aRow, INCLUDE_LAST_CTOR_ARG);
return ModelCtorUtil.buildModelObject(modelCtor, ctorArgValues);
}
private int getNumColsInResultSet(ResultSet aRow) throws SQLException {
return aRow.getMetaData().getColumnCount();
}
private List<Object> getCtorArgValues(Constructor<E> aConstructor, ResultSet aRow, boolean aIncludeLastArg) throws SQLException {
List<Object> result = new ArrayList<Object>();
int columnIdx = 1;
Class<?>[] targetTypes = aConstructor.getParameterTypes();
for(Class argType: targetTypes){
if( isNotLastArg(columnIdx, targetTypes) ){
result.add(fColumnToObject.convert(aRow, columnIdx, argType));
}
else {
if(aIncludeLastArg){
result.add(fColumnToObject.convert(aRow, columnIdx, argType));
}
}
++columnIdx;
}
return result;
}
private boolean isNotLastArg(int aColIndex, Class[] aTargetTypes){
return aColIndex != aTargetTypes.length;
}
/*
Remaining methods all deal with children.
*/
private E buildWithChildList(ResultSet aRow) throws SQLException, ModelCtorException {
fLogger.fine("Building with Child List at end of constructor.");
//first gather parent args (but do not construct yet)
Constructor<E> parentCtor = ModelCtorUtil.getConstructor(fClass, getNumColsForParent(aRow) + 1);
List<Object> ctorArgsMinusChildren = getCtorArgValues(parentCtor, aRow, EXCLUDE_LAST_CTOR_ARG);
fLogger.finest("Parent constructor arg values, minus children : " + ctorArgsMinusChildren);
//build a child for each collection of rows that belongs to the given parent
//identify new parents using changes in FIRST col only
Constructor<?> childCtor = ModelCtorUtil.getConstructor(fChildClass, fNumColsForChildList);
Id parentId = new Id(aRow.getString(FIRST_COLUMN));
fLogger.finest("Building Child List for Parent Id: " + parentId);
Id currentId = null;
List<Object> childList = new ArrayList<Object>();
do {
addChildToList(childCtor, childList, aRow);
aRow.next();
if ( ! aRow.isAfterLast() ){
currentId = new Id(aRow.getString(FIRST_COLUMN));
fLogger.finest("Updated current id: " + currentId);
}
}
while( !aRow.isAfterLast() && currentId.equals(parentId) );
//back up to PREVIOUS row, in preparation for next section/full model object, if any.
aRow.previous();
return buildFinalModelObjectWithChildList(parentCtor, ctorArgsMinusChildren, childList);
}
private int getNumColsForParent(ResultSet aRow) throws SQLException {
int numCols = aRow.getMetaData().getColumnCount();
int result = numCols - fNumColsForChildList;
if ( result < 1){
throw new RuntimeException("Number of columns available for Parent constructor < 1 : " + Util.quote(result));
}
return result;
}
private void addChildToList(Constructor<?> aChildCtor, List<Object> aChildren, ResultSet aRow) throws SQLException, ModelCtorException {
ConvertParam convertParam = BuildImpl.forConvertParam();
if( convertParam.isSupported(aChildCtor.getDeclaringClass())) {
addBaseChildToList(aChildCtor.getDeclaringClass(), aChildren, aRow);
}
else {
//regular model object, not a base object
addRegularChildToList(aChildCtor, aChildren, aRow);
}
}
/** Child object is a regular Model Object, whose ctor takes Base Objects supported by ConvertColumn. */
private void addRegularChildToList(Constructor<?> aChildCtor, List<Object> aChildren, ResultSet aRow) throws SQLException, ModelCtorException {
fLogger.fine("Building a regular child, and adding to the child list.");
Class<?>[] targetTypes = aChildCtor.getParameterTypes();
List<Object> argValues = new ArrayList<Object>();
int columnIdx = getNumColsForParent(aRow) + 1; //ignore the leading columns belonging to parent
//some outer joins will have all child columns null
boolean atLeastOneHasContent = false;
for (Class argType: targetTypes){
Object value = fColumnToObject.convert(aRow, columnIdx, argType);
if ( value != null ){
atLeastOneHasContent = true;
}
argValues.add(value);
++columnIdx;
}
if( atLeastOneHasContent ) {
fLogger.finest("At least one child column has content.");
Object child = ModelCtorUtil.buildModelObject(aChildCtor, argValues);
aChildren.add(child);
}
else {
//do not add anthing to the list of children
fLogger.finest("ALL columns for Child object are empty/null - no child object to construct.");
}
}
/** Child object is simply a Base Object, supported by ConvertColumn. */
private <T> void addBaseChildToList(Class<T> aBaseClass, List<Object> aChildren, ResultSet aRow) throws SQLException {
//Note : this extra branch was added when String was removed from supported Base Objects.
fLogger.fine("Building a base object child, and adding to the child list.");
int lastColumn = getNumColsInResultSet(aRow);
T baseObject = fColumnToObject.convert(aRow, lastColumn, aBaseClass); //possibly-null
aChildren.add(baseObject);
}
private E buildFinalModelObjectWithChildList(Constructor<E> aParentCtor, List<Object> aArgValuesMinusChildren, List<Object> aChildren) throws ModelCtorException {
fLogger.fine("Building complete Parent with Child List.");
List<Object> allArgs = new ArrayList<Object>();
allArgs.addAll(aArgValuesMinusChildren);
allArgs.add(aChildren);
fLogger.finest("Building complete Parent Model Object.");
return ModelCtorUtil.buildModelObject(aParentCtor, allArgs);
}
}