/*
* 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.Iterator;
import java.util.List;
import java.util.Map;
import com.scooterframework.common.exception.GenericException;
import com.scooterframework.common.exception.UnsupportFeatureException;
import com.scooterframework.common.util.Converters;
import com.scooterframework.orm.sqldataexpress.util.OrmObjectFactory;
import com.scooterframework.transaction.ImplicitTransactionManager;
import com.scooterframework.transaction.TransactionManagerUtil;
/**
* <p>AssociatedRecordsHMT class contains a list of ActiveRecord objects and its
* owner in a has-many-through association.</p>
*
* @author (Fei) John Chen
*/
public class AssociatedRecordsHMT extends AssociatedRecords {
public AssociatedRecordsHMT(RecordRelation recordRelation) {
super(recordRelation);
}
public AssociatedRecordsHMT(RecordRelation recordRelation, List<? extends ActiveRecord> records) {
super(recordRelation, records);
}
/**
* Adds a record to the association. If the owner object is
* already in the database, the record will be either inserted or updated in
* the database. If the owner record is not in the database, the record
* will not be saved to the database.
*
* @param record A record to be added to the relation.
* @param joinInput A map of input data for the join record.
* @return updated AssociatedRecords
*/
public AssociatedRecords add(ActiveRecord record, Map<String, Object> joinInput) {
if (record == null) return this;
List<ActiveRecord> records = new ArrayList<ActiveRecord>();
records.add(record);
List<Map<String, Object>> joinInputs = new ArrayList<Map<String, Object>>();
joinInputs.add(joinInput);
return add(records, joinInputs);
}
/**
* Adds a list of records to the association. If the owner object is
* already in the database, the records will be either inserted or updated in
* the database. If the owner record is not in the database, the records
* will not be saved to the database.
*
* @param records a list of records to be added to the relation.
* @return updated AssociatedRecords.
*/
public AssociatedRecords add(List<? extends ActiveRecord> records) {
if (records == null || records.size() == 0) return this;
HasManyThroughRelation hmtRelation = (HasManyThroughRelation)super.getRelation();
Map<String, Object> inputsMap = hmtRelation.getJoinInputs();
int size = records.size();
List<Map<String, Object>> inputsMapList = new ArrayList<Map<String, Object>>(size);
for (int i=0; i<size; i++) {
inputsMapList.add(i, inputsMap);
}
return add(records, inputsMapList);
}
/**
* Adds a list of records to the association. If the owner object is
* already in the database, the records will be either inserted or updated in
* the database. If the owner record is not in the database, the records
* will not be saved to the database.
*
* @param records a list of records to be added to the relation.
* @param joinInputs a list of input data map for the through table.
* @return updated AssociatedRecords.
*/
public AssociatedRecords add(List<? extends ActiveRecord> records, List<Map<String, Object>> joinInputs) {
ImplicitTransactionManager tm = TransactionManagerUtil.getImplicitTransactionManager();
AssociatedRecords assocs = null;
try {
tm.beginTransactionImplicit();
assocs = internal_add(records, joinInputs);
tm.commitTransactionImplicit();
}
catch(GenericException ex) {
tm.rollbackTransactionImplicit();
throw ex;
}
finally {
tm.releaseResourcesImplicit();
}
return assocs;
}
private AssociatedRecords internal_add(List<? extends ActiveRecord> records, List<Map<String, Object>> joinInputs) {
if (records == null || records.size() == 0) return this;
if (joinInputs != null && joinInputs.size() != records.size())
throw new IllegalArgumentException("The size of joinInputs must be the same as records size.");
//retrieve current list
associatedRecords = getRecords();
// make sure the record type is valid
validateRecordType(getRelation().getTargetClass(), records);
// now add the records to db and memory by
if (associatedRecords == null) associatedRecords = new ArrayList<ActiveRecord>();
ActiveRecord owner = getOwner();
if (!owner.isNewRecord()) {
HasManyThroughRelation hmtRelation = (HasManyThroughRelation)getRelation();
Relation acRelation = hmtRelation.getACRelation();
Relation cbRelation = hmtRelation.getCBRelation();
int inputSize = records.size();
for (int i = 0; i < inputSize; i++) {
ActiveRecord record = records.get(i);
if (record == null) continue;
if (record.isNewRecord() || record.isDirty()) record.save();
//must check CB relation type
if (Relation.BELONGS_TO_TYPE.equals(cbRelation.getRelationType())) {
//now construct a join class
ActiveRecord joinRecord = (ActiveRecord)OrmObjectFactory.getInstance().newInstance(hmtRelation.getMiddleC());
if (joinInputs != null) {
Map<String, Object> inputs = joinInputs.get(i);
joinRecord.setData(inputs);
}
//need to populate ALL fks before linking to both ends
AssociationHelper.populateFKInBelongsTo(joinRecord, acRelation.getReverseMappingMap(), owner);
AssociationHelper.populateFKInBelongsTo(joinRecord, cbRelation.getMappingMap(), record);
AssociatedRecord assocCB = joinRecord.associated(cbRelation.getAssociation());
assocCB.storeLoadedAssociatedRecord(record);
//link the middleC record with both ends.
AssociatedRecord assocCA = joinRecord.associated(acRelation.getReverseRelationName());
assocCA.attach(owner);//join record saved, all counters updated
//assocCB.attach(record);//This is not necessary
}
else {
//must be a has-many relation
//Then there is no need to create a middleC class
}
//add the record to memory
associatedRecords.add(record);
}
}
else {
associatedRecords.addAll(records);
}
return this;
}
/**
* <p>Removes target record from the association. The record is not deleted.
* The join record is deleted if the value of the keepJoinRecord input
* is false or the join record depends on the record to be deleted. </p>
*
* <p>If the owner object doesn't exist in database, only those objects that
* have been added to the relation before can be detached.</p>
*
* @param target a record to be detached from the association.
* @param keepJoinRecord if true, keep the join record. Otherwise, delete it.
* @return updated AssociatedRecords.
*/
public AssociatedRecords detach(ActiveRecord target, boolean keepJoinRecord) {
List<ActiveRecord> records = new ArrayList<ActiveRecord>();
if (target != null) records.add(target);
return detach(records, keepJoinRecord);
}
/**
* <p>Removes a list of records from the association. No record is deleted.
* Only the join record is deleted. </p>
*
* <p>If the owner object doesn't exist in database, only those objects that
* have been added to the relation before can be detached.</p>
*
* @param records list of records to be detached.
* @return updated AssociatedRecords.
*/
public AssociatedRecords detach(List<? extends ActiveRecord> records) {
return detach(records, false);
}
/**
* <p>Removes a list of records from the association. No record is deleted.
* The join record is deleted if the value of the <tt>keepJoinRecord</tt>
* input is false or the join record depends on the record to be deleted.</p>
*
* <p>Proper use of keepJoinRecord parameter is good for a three-way join
* association.</p>
*
* <p>If the owner object doesn't exist in database, only those objects that
* have been added to the relation before can be detached.</p>
*
* @param records list of records to be detached.
* @param keepJoinRecord if true, keep the join record. Otherwise, delete it.
* @return updated AssociatedRecords.
*/
public AssociatedRecords detach(List<? extends ActiveRecord> records, boolean keepJoinRecord) {
ImplicitTransactionManager tm = TransactionManagerUtil.getImplicitTransactionManager();
AssociatedRecords assocs = null;
try {
tm.beginTransactionImplicit();
assocs = internal_detach(records, keepJoinRecord);
tm.commitTransactionImplicit();
}
catch(GenericException ex) {
tm.rollbackTransactionImplicit();
throw ex;
}
finally {
tm.releaseResourcesImplicit();
}
return assocs;
}
private AssociatedRecords internal_detach(List<? extends ActiveRecord> records, boolean keepJoinRecord) {
if (records == null || records.size() == 0) return this;
//retrieve current list
associatedRecords = getRecords();
if (ownerIsNew()) {
if (associatedRecords != null) associatedRecords.removeAll(records);
}
else {
validateRecordType(getRelation().getTargetClass(), records);
// update the record in db and remove it from memory
if (associatedRecords != null && associatedRecords.size() > 0) {
HasManyThroughRelation hmtRelation = (HasManyThroughRelation)getRelation();
Relation acRelation = hmtRelation.getACRelation();
Relation cbRelation = hmtRelation.getCBRelation();
// check if the record is in the relation
int inputSize = records.size();
for (int i=0; i<inputSize; i++) {
ActiveRecord record = (ActiveRecord) records.get(i);
if (record == null) continue;
if (record.isNewRecord()) {
associatedRecords.remove(record);
}
else {
if (Relation.BELONGS_TO_TYPE.equals(cbRelation.getRelationType())) {
ActiveRecord joinRecord = getJoinRecord(getOwner(), record);
//detach the middleC record from the link to the record.
if (joinRecord != null) {
Relation caRelation = acRelation.getReverseRelation();
BelongsToRecordRelation caBTRR =
new BelongsToRecordRelation(joinRecord, (BelongsToRelation)caRelation);
AssociatedRecord caAssociatedRecord = new AssociatedRecord(caBTRR, getOwner());
caBTRR.setAssociatedData(caAssociatedRecord);
joinRecord.setRecordRelation(caRelation.getAssociation(), caBTRR);
BelongsToRecordRelation cbBTRR =
new BelongsToRecordRelation(joinRecord, (BelongsToRelation)cbRelation);
AssociatedRecord cbAssociatedRecord = new AssociatedRecord(cbBTRR, record);
cbBTRR.setAssociatedData(cbAssociatedRecord);
joinRecord.setRecordRelation(cbRelation.getAssociation(), cbBTRR);
if (!keepJoinRecord || joinRecord.isDependentOf(record)) {
joinRecord.delete();
}
else {
joinRecord.associated(cbRelation.getAssociation()).detach();
}
}
}
else {
//must be a has-many relation
}
//remove the record from memory
associatedRecords = removeRecordFromList(associatedRecords, record);
}
}
}
}
return this;
}
/**
* <p>Deletes a list objects from the associated list and delete the records in
* database whether the objects are dependent on the owner or not.</p>
*
* <p>The associated records in the join table can either be deleted or
* have its foreign key nullified, depending on how you specify the cascade
* property in the "to-be-deleted" record class. </p>
*
* <p>If the owner object doesn't exist in database, only those objects that
* have been added to the relation before can be detached.</p>
*
* @param records a list of records to be deleted.
* @return updated AssociatedRecords.
*/
public AssociatedRecords delete(List<? extends ActiveRecord> records) {
ImplicitTransactionManager tm = TransactionManagerUtil.getImplicitTransactionManager();
AssociatedRecords assocs = null;
try {
tm.beginTransactionImplicit();
assocs = internal_delete(records);
tm.commitTransactionImplicit();
}
catch(GenericException ex) {
tm.rollbackTransactionImplicit();
throw ex;
}
finally {
tm.releaseResourcesImplicit();
}
return assocs;
}
private AssociatedRecords internal_delete(List<? extends ActiveRecord> records) {
if (records == null || records.size() == 0) return this;
//retrieve current list
associatedRecords = getRecords();
if (ownerIsNew()) {
if (associatedRecords != null) associatedRecords.removeAll(records);
}
else {
validateRecordType(getRelation().getTargetClass(), records);
// update the record in db and remove it from memory
if (associatedRecords != null && associatedRecords.size() > 0) {
// check if the record is in the relation
int inputSize = records.size();
for (int i=0; i<inputSize; i++) {
ActiveRecord record = (ActiveRecord) records.get(i);
if (record == null) continue;
if (record.isNewRecord()) {
associatedRecords.remove(record);
}
else {
record.delete();
//remove the record from memory
associatedRecords = removeRecordFromList(associatedRecords, record);
}
}
}
}
return this;
}
/**
* <p>This feature is not supported for has-many-through relation.</p>
*
* <p>This is equivalent to detach() and add(records).</p>
*
* <p>Removes all existing associated objects from the associated list by
* setting their foreign keys to NULL. The records are not deleted
* unless they depend on the owner. </p>
*
* <p>Adds the new records to the association.</p>
*
* @return updated AssociatedRecords
*/
public AssociatedRecords replace(List<? extends ActiveRecord> records) {
throw new UnsupportFeatureException("replace() is not supported by has-many-through relation.");
}
/**
* Retrieves the join record between the owner and the <tt>target</tt>.
*
* @param target the associated record
* @return the join record
*/
public ActiveRecord getJoinRecord(ActiveRecord target) {
return getJoinRecord(getOwner(), target);
}
/**
* Count number of associated records in the database.
*
* @return number of associated records
*/
protected int countRecordsInDB() {
return ((HasManyThroughRecordRelation)recordRelation).countRecordsInDB();
}
/**
* Retrieves the join record between records A and B. This method is only
* good for the case where CB relation is belongs-to.
*
* @param recordA endA record
* @param recordB endB record
* @return the join record
*/
private ActiveRecord getJoinRecord(ActiveRecord recordA, ActiveRecord recordB) {
HasManyThroughRelation rel = (HasManyThroughRelation)getRelation();
//construct a conditions map
Map<String, Object> conditions = new HashMap<String, Object>();
Map<String, String> acMappingMap = getMappingMap(rel.getACMapping());
for (Map.Entry<String, String> entry : acMappingMap.entrySet()) {
String aFld = entry.getKey();
String cFld = entry.getValue();
conditions.put(cFld, recordA.getField(aFld));
}
Map<String, String> cbMappingMap = getMappingMap(rel.getCBMapping());
for (Map.Entry<String, String> entry : cbMappingMap.entrySet()) {
String cFld = entry.getKey();
String bFld = entry.getValue();
conditions.put(cFld, recordB.getField(bFld));
}
//If this join record is in a category and the endB class is a type of
//the category, then add more to the conditions map.
Class<? extends ActiveRecord> joinClass = rel.getMiddleC();
List<Category> categories = RelationManager.getInstance().getRegisteredCategory(joinClass);
if (categories != null && categories.size() > 0) {
boolean inCategory = false;
Category category = null;
Iterator<Category> itc = categories.iterator();
while(itc.hasNext()) {
category = itc.next();
if (category.isEntityInCategory(rel.getTargetModel())) {
inCategory = true;
break;
}
}
if (inCategory) {
conditions.put(category.getTypeField(), category.getTypeByEntity(rel.getTargetModel()));
}
}
ActiveRecord joinRecord = ActiveRecordUtil.getGateway(joinClass).findFirst(conditions);
return joinRecord;
}
private Map<String, String> getMappingMap(String mapping) {
return Converters.convertStringToMap(mapping);
}
}