/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.orm.activerecord;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.scooterframework.common.logging.LogUtil;
import com.scooterframework.common.util.Converters;
import com.scooterframework.orm.sqldataexpress.object.RowData;
import com.scooterframework.orm.sqldataexpress.object.RowInfo;
import com.scooterframework.orm.sqldataexpress.object.TableData;
import com.scooterframework.orm.sqldataexpress.processor.DataProcessor;
import com.scooterframework.orm.sqldataexpress.util.OrmObjectFactory;
/**
* IncludeHelper class has helper methods for SQL queries with include option.
*
* @author (Fei) John Chen
*/
public class IncludeHelper {
IncludeHelper(Class<? extends ActiveRecord> recordClz,
Map<String, Object> conditions, Map<String, String> options) {
if (options == null)
throw new IllegalArgumentException(
"options cannot be null for IncludeHelper.");
mainHome = ActiveRecordUtil.getHomeInstance(recordClz);
this.conditions = conditions;
this.options = options;
String jType = options.get(ActiveRecordConstants.key_join_type);
if (JOIN_TYPE_INNER.equalsIgnoreCase(jType)) {
joinType = JOIN_TYPE_INNER;
}
String includeString = options.get(ActiveRecordConstants.key_include);
String strictIncludeString = options.get(ActiveRecordConstants.key_strict_include);
if (!isEmpty(includeString)) {
if (!isEmpty(strictIncludeString)) {
throw new IllegalArgumentException("include and strict_include cannot appear together in options " + options);
}
}
else {
if (!isEmpty(strictIncludeString)) {
includeString = strictIncludeString;
joinType = JOIN_TYPE_INNER;
}
else {
throw new IllegalArgumentException("There must be either include or strict_include in options " + options);
}
}
initializeIncludeNodeList(includeString);
}
IncludeHelper(Class<? extends ActiveRecord> recordClz,
String conditionsSQL, Map<String, Object> conditionsSQLData,
Map<String, String> options) {
this(recordClz, null, options);
this.conditionsSQL = conditionsSQL;
this.conditionsSQLData = conditionsSQLData;
}
IncludeHelper(Class<? extends ActiveRecord> recordClz,
Map<String, Object> conditions, Map<String, String> options,
String hmtInnerJoinSQL, String hmtMidCMapping,
Map<String, Object> hmtMidCMapData, String hmtConditionsSQL) {
this(recordClz, conditions, options);
this.hmtInnerJoinSQL = hmtInnerJoinSQL;
this.hmtMidCMapping = hmtMidCMapping;
this.hmtMidCMapData = hmtMidCMapData;
this.hmtConditionsSQL = hmtConditionsSQL;
}
private static boolean isEmpty(String s) {
return (s == null || "".equals(s))?true:false;
}
private void initializeIncludeNodeList(String includeString) {
List<String> includes = Converters.convertStringToUniqueList(includeString.toLowerCase());
Iterator<String> it = includes.iterator();
while(it.hasNext()) {
String include = it.next();
if (include.indexOf(INCLUDE_LINK) != -1) {
constructIncludeNodes(include);
}
else {
includeNodes.add(constructIncludeNode(include, mainHome, null, mainHome.getClass()));
}
}
}
private void constructIncludeNodes(String includes) {
List<String> nodes = Converters.convertStringToUniqueList(includes, INCLUDE_LINK);
int index = 0;
IncludeNode previous = null;
IncludeNode current = null;
Iterator<String> it = nodes.iterator();
while(it.hasNext()) {
String include = it.next();
if (index == 0) {
current = constructIncludeNode(include, mainHome, null, mainHome.getClass());
}
else {
current = constructIncludeNode(include, mainHome, previous, previous.getHomeInstance().getClass());
previous.setNext(current);
}
includeNodes.add(current);
previous = current;
index++;
}
}
private IncludeNode constructIncludeNode(String include, ActiveRecord controlHome,
IncludeNode previousIncludeNode, Class<? extends ActiveRecord> endAClz) {
String includeName = include.toLowerCase();
int order = getOrder();
boolean useTableAlias = !checkUnique(includeName);
Relation relation = RelationManager.getInstance().getRelation(endAClz, includeName);
if (relation == null) {
throw new UndefinedRelationException(ActiveRecordUtil.getModelName(endAClz), include);
}
IncludeNode node = new IncludeNode(includeName, controlHome, order, previousIncludeNode, relation, useTableAlias, joinType);
//try a better looking alias name
if (useTableAlias) {
String endAMapping = node.getEndAMappingName();
String alias = endAMapping + "_" + node.getHomeInstance().getTableName();
if (checkUnique(alias)) {
node.setTableAlias(alias);
}
}
return node;
}
private int getOrder() {
return ++order;
}
private boolean checkUnique(String entity) {
boolean unique = false;
if (!uniqueIncludes.contains(entity)) {
unique = true;
uniqueIncludes.add(entity);
}
return unique;
}
/**
* Constructs a SQL query for the include case.
*
* @return a Map containing both the SQL and input data
*/
public Map<String, Object> getConstructedSqlQuery() {
Map<String, Object> inputsAndSql = new HashMap<String, Object>();
String findSQL = "";
String conditionSql = null;
StringBuilder sqlSelectSB = new StringBuilder();
//construct select query
boolean useUnique = false;
if (options != null && options.size() > 0) {
String unique = options.get(ActiveRecordConstants.key_unique);
if ("true".equalsIgnoreCase(unique)) {
useUnique = true;
}
conditionSql = options.get(ActiveRecordConstants.key_conditions_sql);
}
if (useUnique) {
sqlSelectSB.append("SELECT DISTINCT ");
}
else {
sqlSelectSB.append("SELECT ");
}
StringBuilder sqlJoinSB = new StringBuilder();
String mainTableName = mainHome.getTableName();
String[] columnNames = getAllowedColumnNames();
sqlSelectSB.append(IncludeNode.getSqlSelectPart(mainTableName, columnNames));
Iterator<IncludeNode> itx = includeNodes.iterator();
while(itx.hasNext()) {
IncludeNode node = itx.next();
if (node == null) continue;
sqlSelectSB.append(", ").append(node.toSqlSelectPart());
sqlJoinSB.append(node.toSqlJoinPart());
}
sqlSelectSB.append(" FROM ").append(mainTableName).append(sqlJoinSB);
findSQL = sqlSelectSB.toString();
//Section of code for conditionsSQL
boolean isConditionsSQL = (conditionsSQL != null && !"".equals(conditionsSQL.trim()))?true:false;
//Section of code for hmt
boolean isHMT = (hmtInnerJoinSQL != null)?true:false;
if (isHMT) {
findSQL = findSQL + " " + hmtInnerJoinSQL;
}
//construct where clause
Map<String, Object> inputs = new HashMap<String, Object>();
String whereClause = "";
boolean useWhere = false;
if (conditions != null && conditions.size() > 0) {
int position = 1;
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
String columnName = entry.getKey();
//skip system keys
if (columnName == null || columnName.startsWith("_") ||
columnName.toUpperCase().startsWith(DataProcessor.framework_input_key_prefix.toUpperCase()) ||
((columnName.indexOf('.') == -1) && (!mainHome.getRowInfo().isValidColumnName(columnName)))
) continue;
Object conditionData = entry.getValue();
if (columnName.indexOf('.') == -1) {
whereClause += mainTableName + "." + columnName + " = ? AND ";
}
else {
whereClause += columnName + " = ? AND ";
}
//inputs.put(columnName, conditionData);
inputs.put(position + "", conditionData);
useWhere = true;
position = position + 1;
}
if (whereClause.endsWith("AND ")) {
int lastAnd = whereClause.lastIndexOf("AND ");
whereClause = whereClause.substring(0, lastAnd);
}
inputsAndSql.putAll(conditions);
}
boolean whereUsed = false;
if (useWhere) {
findSQL += " WHERE " + whereClause;
if (conditionSql != null && !"".equals(conditionSql)) {
findSQL += " AND (" + conditionSql + ")";
}
whereUsed = true;
}
else {
if (conditionSql != null && !"".equals(conditionSql)) {
findSQL += " WHERE (" + conditionSql + ")";
whereUsed = true;
}
}
if (isConditionsSQL) {
if (whereUsed) {
findSQL = findSQL + " AND (" + conditionsSQL + ")";
}
else {
findSQL = findSQL + " WHERE (" + conditionsSQL + ")";
whereUsed = true;
}
if (conditionsSQLData != null) {
inputsAndSql.putAll(conditionsSQLData);
}
}
//Section of code for hmt
if (isHMT) {
String hmtString = hmtMidCMapping;
if (hmtConditionsSQL != null && !"".equals(hmtConditionsSQL)) {
hmtString = hmtString + hmtConditionsSQL;
}
if (whereUsed) {
findSQL = findSQL + " AND (" + hmtString + ")";
}
else {
findSQL = findSQL + " WHERE (" + hmtString + ")";
}
inputsAndSql.putAll(hmtMidCMapData);
}
findSQL += QueryHelper.getAllSelectQueryClauses(options);
if (options != null) inputsAndSql.putAll(options);
log.debug("find SQL = " + findSQL);
inputsAndSql.put(ActiveRecordConstants.key_finder_sql, findSQL);
inputsAndSql.putAll(inputs);
return inputsAndSql;
}
/**
* Returns join part of select query.
*
* @return a SQL fragment for join
*/
public String getConstructedJoinQuery() {
StringBuilder sqlJoinSB = new StringBuilder();
Iterator<IncludeNode> itx = includeNodes.iterator();
while(itx.hasNext()) {
IncludeNode node = itx.next();
if (node == null) continue;
sqlJoinSB.append(node.toSqlJoinPart());
}
return sqlJoinSB.toString();
}
private String[] getAllowedColumnNames() {
String[] columnNames = null;
boolean useColumns = false;
boolean exColumns = false;
if (options != null && options.size() > 0) {
String columns = options.get(ActiveRecordConstants.key_columns);
String excolumns = options.get(ActiveRecordConstants.key_ex_columns);
if (columns != null) {
useColumns = true;
}
if (excolumns != null) {
exColumns = true;
}
}
if (!useColumns && !exColumns) {
columnNames = mainHome.getRowInfo().getColumnNames();
}
else if (useColumns) {
String columnsStr = options.get(ActiveRecordConstants.key_columns);
List<String> columns = Converters.convertStringToUniqueList(columnsStr.toUpperCase());
int length = columns.size();
columnNames = new String[length];
for (int i=0; i<length; i++) {
columnNames[i] = columns.get(i);
}
}
else if (exColumns) {
String excolumnsStr = options.get(ActiveRecordConstants.key_ex_columns);
List<String> excolumns = Converters.convertStringToUniqueList(excolumnsStr.toUpperCase());
String[] columns = mainHome.getRowInfo().getColumnNames();
int length = columns.length;
columnNames = new String[length - excolumns.size()];
int index = 0;
for (int i=0; i<length; i++) {
String column = columns[i];
if (excolumns.contains(column)) continue;
columnNames[index] = column;
index++;
}
}
return columnNames;
}
/**
* Organizes raw data retrieved from database into a list of associated
* ActiveRecord instances.
*
* @param retrievedTableData TableData from database
* @return List of associated records.
*/
public List<ActiveRecord> organizeData(TableData retrievedTableData) {
if (retrievedTableData == null) return null;
//scan for main home
String mainTableName = mainHome.getTableName();
recordDataMap.put(mainTableName, retrieveRecordDataList(mainTableName, mainHome, retrievedTableData));
//scan for each include node
int totalNodes = includeNodes.size();
for (int i=0; i<totalNodes; i++) {
IncludeNode node = includeNodes.get(i);
if (node == null) continue;
String tableMappingName = node.getMappingName();
recordDataMap.put(tableMappingName, retrieveRecordDataList(tableMappingName, node.getHomeInstance(), retrievedTableData));
}
//associate everything
constructAssociation(mainTableName);
//now return the main list
List<RecordData> mainRecordData = recordDataMap.get(mainTableName);
if (mainRecordData == null) return null;
List<ActiveRecord> list = new ArrayList<ActiveRecord>();
int size = mainRecordData.size();
for (int i=0; i<size; i++) {
list.add((mainRecordData.get(i)).getRecord());
}
return list;
}
private List<RecordData> retrieveRecordDataList(String tableMappingName, ActiveRecord entityHome, TableData retrievedTableData) {
int totalRows = retrievedTableData.getTableSize();
if (totalRows == 0) return null;
//key is keyString, value is RecordData
Map<String, RecordData> rowKeyMap = new HashMap<String, RecordData>();
List<RecordData> results = new ArrayList<RecordData>();
for (int i=0; i < totalRows; i++) {
RowData resultRow = (RowData)retrievedTableData.getRow(i);
RowData entityRow = constructRowData(resultRow, tableMappingName, entityHome);
if (entityRow == null) continue;
String keyDataString = getKeyDataString(entityRow);
if (rowKeyMap.containsKey(keyDataString)) {
RecordData recordData = (RecordData)rowKeyMap.get(keyDataString);
recordData.addIndex(i);
}
else {
RecordData recordData = new RecordData(entityHome, entityRow);
rowKeyMap.put(keyDataString, recordData);
recordData.addIndex(i);
results.add(recordData);
}
}
return results;
}
//Creates a new RowData based on a retrieved row for an entity.
private RowData constructRowData(RowData retrievedRow, String tableMappingName, ActiveRecord entityHome) {
RowInfo ri = entityHome.getRowInfo();
boolean hasPrimaryKey = ri.hasPrimaryKey();
boolean hasOnlyNullData = true;
String[] columnNames = ri.getColumnNames();
int dimension = columnNames.length;
Object[] colData = new Object[dimension];
for (int i=0; i<dimension; i++) {
String columnName = columnNames[i];
colData[i] = retrievedRow.getField(tableMappingName + "_" + columnName);
if (hasPrimaryKey) {
if ((colData[i] == null) && ri.isPrimaryKeyColumn(columnName)) {
return null;
}
}
if (colData[i] != null) {
hasOnlyNullData = false;
}
}
if (hasOnlyNullData) return null;
return new RowData(ri, colData);
}
private String getKeyDataString(RowData rd) {
String key = "";
if (rd.hasPrimaryKey()) {
key = rd.getPrimaryKeyDataString();
}
else {
key = rd.getDataMap().toString();
}
return key;
}
//return map: key is row index, value is ActiveRecord instance.
private Map<Integer, ActiveRecord> constructIndexRecordMap(List<RecordData> recDataList) {
if (recDataList == null) return null;
Map<Integer, ActiveRecord> indexRecordMap = new HashMap<Integer, ActiveRecord>();
int recDataSize = recDataList.size();
for (int i=0; i<recDataSize; i++) {
RecordData recordData = recDataList.get(i);
ActiveRecord record = recordData.getRecord();
List<Integer> indices = recordData.getRowIndexList();
int totalRows = indices.size();
for (int j = 0; j < totalRows; j++) {
Integer index = indices.get(j);
indexRecordMap.put(index, record);
}
}
return indexRecordMap;
}
private void constructAssociation(String endATableMappingName) {
List<RecordData> recordDataList = recordDataMap.get(endATableMappingName);
if (recordDataList == null) return;
//WARNING: This condition should only be used for left outer join.
List<IncludeNode> nextNodes = getTargetNodeList(endATableMappingName);
if (nextNodes == null || nextNodes.size() == 0) return;
Iterator<RecordData> it = recordDataList.iterator();
while(it.hasNext()) {
RecordData recordData = it.next();
if (recordData == null) continue;
ActiveRecord owner = recordData.getRecord();
Iterator<IncludeNode> itNode = nextNodes.iterator();
while(itNode.hasNext()) {
IncludeNode includeNode = itNode.next();
String endBTableMappingName = includeNode.getMappingName();
Map<Integer, ActiveRecord> targets = constructIndexRecordMap(recordDataMap.get(endBTableMappingName));
Relation relation = includeNode.getRelation();
if (Relation.BELONGS_TO_TYPE.equals(relation.getRelationType()) ||
Relation.HAS_ONE_TYPE.equals(relation.getRelationType())) {
processAssociatedRecord(owner, recordData.getFirstIndex(), includeNode.getIncludeName(), targets);
}
else if (Relation.HAS_MANY_TYPE.equals(relation.getRelationType()) ||
Relation.HAS_MANY_THROUGH_TYPE.equals(relation.getRelationType())) {
processAssociatedRecordsHM(owner, recordData.getRowIndexList(), includeNode.getIncludeName(), targets);
}
constructAssociation(endBTableMappingName);
}
}
}
//get a list of include nodes whose previous node's mapping name
//matches the input mapping name.
private List<IncludeNode> getTargetNodeList(String mappingName) {
List<IncludeNode> result = new ArrayList<IncludeNode>();
int totalIncludes = includeNodes.size();
for (int i=0; i<totalIncludes; i++) {
IncludeNode in = includeNodes.get(i);
if (in != null && mappingName.equalsIgnoreCase(in.getEndAMappingName())) {
result.add(in);
}
}
return result;
}
private void processAssociatedRecord(ActiveRecord owner, Integer rowIndex,
String include, Map<Integer, ActiveRecord> targets) {
//create a new ActiveRecord instance
ActiveRecord target = targets.get(rowIndex);
//create RecordRelation
RecordRelation rr = owner.getRecordRelation(include);
//create AssociatedRecord
AssociatedRecord ar = new AssociatedRecord(rr, target);
//store the AssociatedRecord
rr.setAssociatedData(ar);
}
private void processAssociatedRecordsHM(ActiveRecord owner, List<Integer> rowIndexList,
String include, Map<Integer, ActiveRecord> targets) {
int listLength = rowIndexList.size();
List<ActiveRecord> targetRecords = new ArrayList<ActiveRecord>(listLength);
for (int i=0; i<listLength; i++) {
Integer rowIndex = (Integer)rowIndexList.get(i);
ActiveRecord target = targets.get(rowIndex);
if (target != null && !targetRecords.contains(target)) targetRecords.add(target);
}
//create RecordRelation
RecordRelation rr = owner.getRecordRelation(include);
//create AssociatedRecords
String relationType = rr.getRelation().getRelationType();
AssociatedRecords ars = null;
if (Relation.HAS_MANY_TYPE.equals(relationType)) {
ars = new AssociatedRecordsHM(rr, targetRecords);
}
else if (Relation.HAS_MANY_THROUGH_TYPE.equals(relationType)) {
ars = new AssociatedRecordsHMT(rr, targetRecords);
}
//store the AssociatedRecords
rr.setAssociatedData(ars);
}
private ActiveRecord findOrCreateRecord(Class<? extends ActiveRecord> recordHomeClass, RowData rowData) {
if (rowData == null) return null;
String rowKey = "";
if (rowData.hasPrimaryKey()) {
rowKey = rowData.getPrimaryKeyDataString();
if (rowKey == null) return null;
}
else {
rowKey = rowData.getDataMap().toString();
}
String recordKey = recordHomeClass.getName() + "_" + rowKey;
ActiveRecord record = allRecords.get(recordKey);
if (record == null) {
record = (ActiveRecord)OrmObjectFactory.getInstance().newInstance(recordHomeClass);
record.populateDataFromDatabase(rowData);
allRecords.put(recordKey, record);
}
return record;
}
public static final String JOIN_TYPE_INNER = "INNER JOIN";
public static final String JOIN_TYPE_LEFT_OUTER = "LEFT OUTER JOIN";
public static final String INCLUDE_LINK = "=>";
private String joinType = JOIN_TYPE_LEFT_OUTER;
private ActiveRecord mainHome;
private Map<String, Object> conditions;
private String conditionsSQL;
private Map<String, Object> conditionsSQLData;
private Map<String, String> options;
private int order = 0;
private Set<String> uniqueIncludes = new HashSet<String>();
private List<IncludeNode> includeNodes = new ArrayList<IncludeNode>();
//recordDataMap: key is mapping name, value is list of RecordData
Map<String, List<RecordData>> recordDataMap = new HashMap<String, List<RecordData>>();
/**
* A map holds
*/
private Map<String, ActiveRecord> allRecords = new HashMap<String, ActiveRecord>();
private String hmtInnerJoinSQL;
private String hmtMidCMapping;
private Map<String, Object> hmtMidCMapData;
private String hmtConditionsSQL;
private LogUtil log = LogUtil.getLogger(this.getClass().getName());
class RecordData {
private ActiveRecord rowRecord;
private List<Integer> rowIndexList = new ArrayList<Integer>();
RecordData(ActiveRecord recordHome, RowData rowData) {
rowRecord = (ActiveRecord)findOrCreateRecord(recordHome.getClass(), rowData);
}
void addIndex(int i) {
rowIndexList.add(Integer.valueOf(i));
}
Integer getFirstIndex() {
if (rowIndexList.size() == 0) return null;
return (Integer)rowIndexList.get(0);
}
List<Integer> getRowIndexList() {
return rowIndexList;
}
ActiveRecord getRecord() {
return rowRecord;
}
}
}