/* * 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.HashMap; import java.util.Map; import com.scooterframework.common.util.Converters; /** * Helper class has helper methods for ActiveRecord. * * @author (Fei) John Chen */ public class AssociationHelper { /** * This method adds a bunch of methods in many classes. * <ol> * <li> A has-many-through association from owner to each target class.</li> * <li> A has-many association from each target to through class.</li> * <li> A has-many-through association from each target to owner class.</li> * <li> A belongs-to association from through to each target class.</li> * </ol> * * In order to establish the associations, the method assumes the following: * <ol> * <li> The type value of the category type column is the model name of * each corresponding target class.</li> * <li> The primary key of each target class is "id".</li> * <li> The mapping string between each target class and through class is * "id= category's id column".</li> * <li> The association property from each target to through contains "cascade: delete".</li> * </ol> * * <p> * If any of the above assumptions are not satisfied, you need to use the * other <tt>hasManyInCategoryThrough </tt> method which gives you more * control on specifying the associations. * </p> * * <p>Example usage: </p> * <p>Assuming there are image files and text files in a folder. We create * three models: images, texts, folders. We also use linkings model to * link folders with images and texts files. We will create the following * classes:</p> * * <pre> * CREATE TABLE linkings ( * id INTEGER AUTO_INCREMENT, * folder_id INTEGER, * linkable_id INTEGER, * linkable_type VARCHAR(20), * PRIMARY KEY(id) * ) * * class Linking extends ActiveRecord { * public void registerRelations() { * belongsTo(Folder.class); * belongsToCategory("linkable"); * } * } * * class Folder extends ActiveRecord { * public void registerRelations() { * hasMany(Linking.class); * AssociationHelper.hasManyInCategoryThrough(Folder.class, * new Class[]{Image.class, Text.class}, * "linkable", Linking.class); * } * } * * class Image extends ActiveRecord { * } * * class Text extends ActiveRecord { * } * </pre> * * The following codes show how to get total of ownership for a customer: * <pre> * //Find all ownerships of a customer: * ActiveRecord customerHome = ActiveRecordUtil.getHomeInstance(Customer.class); * ActiveRecord customer = customerHome.find("id=1"); * int total = customer.allAssociatedInCategory("ownerable").size(); * </pre> * * It is also easy to add a dvd to the ownership of the customer: * <pre> * Assign a dvd to a customer: * ActiveRecord dvdHome = ActiveRecordUtil.getHomeInstance(Dvd.class); * ActiveRecord dvd = dvdHome.find("id=4"); * List dvds = customer.allAssociatedInCategory("ownerable").add(dvd).getRecords(); * </pre> * * @param owner owner class * @param targets array of target classes * @param category the category which the targets act as * @param through the middle join class between owner and targets */ public static void hasManyInCategoryThrough(Class<? extends ActiveRecord> owner, Class<? extends ActiveRecord>[] targets, String category, Class<? extends ActiveRecord> through) { if (targets == null || targets.length == 0) { throw new IllegalArgumentException("Target array cannot be empty."); } //make sure category center is loaded first RelationManager.getInstance().registerRelations(through); Category categoryInstance = RelationManager.getInstance().getCategory(category); if (categoryInstance == null) { throw new UnregisteredCategoryException(category); } String idField = categoryInstance.getIdField(); String typeField = categoryInstance.getTypeField(); String cTableName = ActiveRecordUtil.getTableName(through); int targetTotal = targets.length; String[] abProperties = new String[targetTotal]; String[] types = new String[targetTotal]; String relationType = Relation.HAS_MANY_TYPE; String[] bcProperties = new String[targetTotal]; @SuppressWarnings("unchecked") Map<String, Object>[] joinInputs = new HashMap[targetTotal]; String[] cbProperties = new String[targetTotal]; String cbMapping = ActiveRecordConstants.key_mapping + ": " + idField + "=id; "; for (int i=0; i<targetTotal; i++) { types[i] = ActiveRecordUtil.getModelName(targets[i]); String throughTypeCondition = ActiveRecordConstants.key_conditions_sql + ": " + cTableName + "." + typeField + "='" + types[i] + "'"; abProperties[i] = throughTypeCondition; bcProperties[i] = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " + throughTypeCondition + "; " + ActiveRecordConstants.key_cascade + ": delete"; Map<String, Object> inputs = new HashMap<String, Object>(); inputs.put(typeField, types[i]); joinInputs[i] = inputs; cbProperties[i] = cbMapping; } //baProperties are null. hasManyInCategoryThrough(owner, targets, category, through, joinInputs, abProperties, types, relationType, bcProperties, joinInputs, null, null); } /** * This method adds a bunch of methods in many classes. * <pre> * <li> A has-many-through association from owner to each target class.</li> * <li> A has-many association from each target to through class.</li> * <li> A has-many-through association from each target to owner class.</li> * <li> A belongs-to association from through to each target class.</li> * </pre> * * Assuming owner class is A, target class is B, through class is C, * <pre> * <tt>abProperties</tt> is join properties from A to B, * <tt>bcProperties</tt> is join properties from B to C, * <tt>cbProperties</tt> is join properties from C to B, * <tt>baProperties</tt> is join properties from B to A. * </pre> * * @param owner owner class * @param targets array of target classes * @param category the category which the targets act as * @param through the middle join class between owner and targets * @param acJoinInputs array of data map for the join through table. * @param abProperties properties from owner to target class * @param types array of join types in the category, default to model name * @param relationType either has-many or has-one * @param bcProperties array of properties from each target to through class * @param bcJoinInputs array of data map for the join through table. * @param cbProperties array of properties from through to each target class * @param baProperties array of properties from each target to owner class */ public static void hasManyInCategoryThrough(Class<? extends ActiveRecord> owner, Class<? extends ActiveRecord>[] targets, String category, Class<? extends ActiveRecord> through, Map<String, Object>[] acJoinInputs, String[] abProperties, String[] types, String relationType, String[] bcProperties, Map<String, Object>[] bcJoinInputs, String[] cbProperties, String[] baProperties) { if (targets == null || targets.length == 0) { throw new IllegalArgumentException("Target array cannot be empty."); } //make sure category center is loaded first RelationManager.getInstance().registerRelations(through); Category categoryInstance = RelationManager.getInstance().getCategory(category); if (categoryInstance == null) { throw new UnregisteredCategoryException(category); } String idField = categoryInstance.getIdField(); String typeField = categoryInstance.getTypeField(); String cTableName = ActiveRecordUtil.getTableName(through); ActiveRecord ownerHome = ActiveRecordUtil.getHomeInstance(owner); //prepare int targetTotal = targets.length; if (abProperties == null) abProperties = new String[targetTotal]; if (bcProperties == null) bcProperties = new String[targetTotal]; if (cbProperties == null) cbProperties = new String[targetTotal]; if (baProperties == null) baProperties = new String[targetTotal]; String cbMappingProperty = ActiveRecordConstants.key_mapping + ": " + idField + "=id; "; //#1, #2, #4, #3 for (int i=0; i<targetTotal; i++) { Class<? extends ActiveRecord> target = targets[i]; String targetEntityName = ActiveRecordUtil.getModelName(targets[i]); String type = ""; if (types != null) type = types[i]; if (type == null) type = targetEntityName; String throughTypeCondition = ActiveRecordConstants.key_conditions_sql + ": " + cTableName + "." + typeField + "='" + type + "'"; String abProperty = abProperties[i]; if (abProperty == null) { abProperty = throughTypeCondition; } else { if (abProperty.indexOf(ActiveRecordConstants.key_conditions_sql) == -1) { abProperty = throughTypeCondition + "; " + abProperty; } } String bcProperty = bcProperties[i]; if (bcProperty == null) { bcProperty = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " + throughTypeCondition + "; cascade: delete"; } else { if (bcProperty.indexOf(ActiveRecordConstants.key_conditions_sql) == -1) { bcProperty = throughTypeCondition + "; " + bcProperty; } if (bcProperty.indexOf(ActiveRecordConstants.key_mapping) == -1) { bcProperty = ActiveRecordConstants.key_mapping + ": id=" + idField + "; " + bcProperty; } if (bcProperty.indexOf(ActiveRecordConstants.key_cascade) == -1) { bcProperty = ActiveRecordConstants.key_cascade + ": delete" + "; " + bcProperty; } } Map<String, Object> acJoinInputsMap = acJoinInputs[i]; if (acJoinInputsMap == null) { acJoinInputsMap = new HashMap<String, Object>(); } if (acJoinInputsMap.size() == 0) { acJoinInputsMap.put(typeField, type); } String cbProperty = cbProperties[i]; if (cbProperty == null) { cbProperty = cbMappingProperty; } else { if (cbProperty.indexOf(ActiveRecordConstants.key_mapping) == -1) { cbProperty = cbMappingProperty + "; " + cbProperty; } } //#2. A has-many association from each target to through class. //#4. A belongs-to association from through to each target class. ActiveRecord targetHome = ActiveRecordUtil.getHomeInstance(target); targetHome.actAsInCategory(type, category, relationType, through, bcProperty, cbProperty); //#1. A has-many-through association from owner to each target class. //Note: need to add a has-many relation between owner and through // as this is a prerequisit for setting up a has-many-through relation. if (RelationManager.getInstance().existsHasManyRelationBetween(owner, through)) { ownerHome.hasMany(through); } ownerHome.hasManyThrough(target, through, abProperty, acJoinInputsMap); Map<String, Object> bcJoinInputsMap = bcJoinInputs[i]; if (bcJoinInputsMap == null) { bcJoinInputsMap = new HashMap<String, Object>(); } if (bcJoinInputsMap.size() == 0) { bcJoinInputsMap.put(typeField, type); } //#3. A has-many-through association from each target to owner class. //Note: need to add a belongs-to relation between through and owner // as this is a prerequisit for setting up a has-many-through relation. if (RelationManager.getInstance().existsBelongsToRelationBetween(through, owner)) { ActiveRecord throughHome = ActiveRecordUtil.getHomeInstance(through); throughHome.belongsTo(owner); } targetHome.hasManyThrough(owner, through, baProperties[i], bcJoinInputsMap); } } /** * Populates foreign key value in a belongs-to relation. In a belongs-to * relation, the <tt>owner</tt> record should hold the foreign key value. * Therefore, the foreign-key fields in the <tt>owner</tt> record is going * to be set with data of the corresponding fields from the <tt>target</tt> * record. * * @param owner the owner record of the relation * @param mappingMap relation mapping from owner to target * @param target the target record in the relation */ public static void populateFKInBelongsTo(ActiveRecord owner, Map<String, String> mappingMap, ActiveRecord target) { if (owner == null || target == null) return; for (Map.Entry<String, String> entry : mappingMap.entrySet()) { owner.setData(entry.getKey(), target.getField(entry.getValue())); } } /** * Populates foreign key value in a has-many relation. In a has-many * relation, the <tt>target</tt> record should hold the foreign key value. * Therefore, the foreign-key fields in the <tt>target</tt> record is going * to be set with data of the corresponding fields from the <tt>owner</tt> * record. * * @param owner the owner record of the relation * @param mappingMap relation mapping from owner to target * @param target the target record in the relation */ public static void populateFKInHasMany(ActiveRecord owner, Map<String, String> mappingMap, ActiveRecord target) { populateFKInBelongsTo(target, Converters.reverseMap(mappingMap), owner); } /** * Populates foreign key value in a has-one relation. In a has-one * relation, the <tt>target</tt> record should hold the foreign key value. * Therefore, the foreign-key fields in the <tt>target</tt> record is going * to be set with data of the corresponding fields from the <tt>owner</tt> * record. * * @param owner the owner record of the relation * @param mappingMap relation mapping from owner to target * @param target the target record in the relation */ public static void populateFKInHasOne(ActiveRecord owner, Map<String, String> mappingMap, ActiveRecord target) { populateFKInBelongsTo(target, Converters.reverseMap(mappingMap), owner); } }