/* * 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.admin.EnvConfig; import com.scooterframework.common.util.Converters; import com.scooterframework.common.util.StringUtil; import com.scooterframework.common.util.WordUtil; /** * Relation class has information about relationship between objects. * There are three types of binary relations supported: * <pre> * 1. has-one: A has one B. * 2. has-many: A has many B. * 3. belongs-to: A belongs to B. * </pre> * * For has-many-through relation, the HasManyThroughRelation subclass * should be used. * * @author (Fei) John Chen */ abstract public class Relation { public Relation(Class<? extends ActiveRecord> ownerClass, String type, String associationId, String targetModel) { this.ownerClass = ownerClass; this.type = type; this.associationId = associationId; this.targetModel = targetModel; } /** * Returns the relation type. * * @return the relation type */ public String getRelationType() { return type; } /** * Returns the association target. * * @return the association target */ public String getAssociation() { return associationId; } /** * <p>Returns the type of the reverse relation. For <tt>has-one</tt> and * <tt>has-many</tt> relations, the reverse relation type must always be of * <tt>belongs-to</tt>.</p> * * <p>To ease any confusion, it is better to state reverse relation by * using the <tt>reverse</tt> key word in the properties attribute when * declaring a relation.</p> * * <p>This is mostly used by a belongs-to class to figure out if the * reverse relation type is has-one or has-many.</p> * * @return the reverse relation type */ public String getReverseRelationType() { return getReverseRelation().getRelationType(); } /** * <p>Returns reverse relation.</p> * * <p>To ease any confusion, it is better to state reverse relation by * using the <tt>reverse</tt> key word in the properties attribute when * declaring a relation.</p> * * <p>The reverse relation for a has-one or has-many relation is always * of belongs-to type.</p> * * @return reverse relation */ public Relation getReverseRelation() { Relation reverseRelation = null; String reverseAssociationId = properties.get(REVERSE_RELATION); if (reverseAssociationId != null) { reverseRelation = RelationManager.getInstance().getRelation(getTargetClass(), reverseAssociationId); } else { if (HAS_ONE_TYPE.equals(getRelationType()) || HAS_MANY_TYPE.equals(getRelationType())) { //the reverse relation must be of beongs_to type reverseRelation = RelationManager.getInstance().getRelation(getTargetClass(), getOwnerModel()); if (reverseRelation == null || !BELONGS_TO_TYPE.equals(reverseRelation.getRelationType())) { reverseRelation = RelationManager.getInstance().getBelongsToRelationBetween(getTargetClass(), getOwnerClass()); } if (reverseRelation == null) { throw new UndefinedRelationException(getTargetModel(), getOwnerModel()); } } else if (BELONGS_TO_TYPE.equals(getRelationType())) { //the reverse relation must be of has-many or has-one type reverseRelation = RelationManager.getInstance().getRelation(getTargetClass(), WordUtil.pluralize(getOwnerModel())); if (reverseRelation == null) { reverseRelation = RelationManager.getInstance().getRelation(getTargetClass(), getOwnerModel()); if (reverseRelation == null) { reverseRelation = RelationManager.getInstance().getHasManyRelationBetween(getTargetClass(), getOwnerClass()); if (reverseRelation == null) { reverseRelation = RelationManager.getInstance().getHasOneRelationBetween(getTargetClass(), getOwnerClass()); } } } } //has-many-through type should use the reverse key word to define a reverse relation } if (reverseRelation == null) { throw new UndefinedReverseRelationException(getOwnerModel(), getTargetModel()); } return reverseRelation; } /** * <p>Returns reverse relation association id.</p> * * <p>To ease any confusion, it is better to state reverse relation by * using the <tt>reverse</tt> key word in the properties attribute when * declaring a relation.</p> * * @return reverse relation name */ public String getReverseRelationName() { return getReverseRelation().getAssociation(); } /** * Returns owner class. */ public Class<? extends ActiveRecord> getOwnerClass() { return ownerClass; } /** * Returns target class. */ public Class<? extends ActiveRecord> getTargetClass() { if (targetClass == null) { targetClass = ActiveRecordUtil.getHomeInstance(EnvConfig.getInstance().getModelClassName(getTargetModel())).getClass(); } return targetClass; } /** * Sets target class. * * @param targetClass target class */ public void setTargetClass(Class<? extends ActiveRecord> targetClass) { this.targetClass = targetClass; } /** * Returns owner model name. */ public String getOwnerModel() { if (ownerModel == null) { ownerModel = ActiveRecordUtil.getModelName(ownerClass); } return ownerModel; } /** * Returns target model name. */ public String getTargetModel() { return targetModel; } /** * Returns the mapping string. * * The mapping string is like: "id=order_id" where id is primary key of * Order record and order_id is foreign-key of Invoice record. */ public String getMapping() { return mapping; } /** * Returns the reverse mapping. */ public String getReverseMapping() { return StringUtil.reverseMapping(mapping); } /** * Sets the mapping string. */ void setMapping(String mapping) { this.mapping = mapping; properties.put(ActiveRecordConstants.key_mapping, mapping); } /** * Returns an array of all strings on the left-side of the mapping. * * @return an array of all strings on the left-side of the mapping. */ public String[] getLeftSideMappingItems() { Map<String, String> mappingMap = Converters.convertStringToMap(getMapping()); String[] sa = new String[mappingMap.size()]; int i = 0; for (String s : mappingMap.keySet()) { sa[i++] = s; } return sa; } /** * Returns an array of all strings on the right-side of the mapping. * * @return an array of all strings on the right-side of the mapping. */ public String[] getRightSideMappingItems() { Map<String, String> mappingMap = Converters.convertStringToMap(getMapping()); String[] sa = new String[mappingMap.size()]; int i = 0; for (String s : mappingMap.values()) { sa[i++] = s; } return sa; } /** * Returns a Map of mapping string * * For belongs-to relation, the key is the foreign-key column of Class A, * while the corresponding value is the primary-key field of Class B. * * For has-one and has-many relations, the key is the primary-key field * of Class A, while the corresponding value is the foreign-key field * of Class B. * * @return a Map of mapping string */ public Map<String, String> getMappingMap() { return Converters.convertStringToMap(getMapping()); } public Map<String, String> getReverseMappingMap() { return Converters.convertStringToMap(getReverseMapping()); } /** * Returns the property map. */ public Map<String, String> getProperties() { return properties; } /** * Returns the conditions_sql in the property map. */ public String getConditionsString() { return properties.get(ActiveRecordConstants.key_conditions_sql); } /** * Returns the conditions_sql in the property map. Replaces table name * with mapping alias table name. */ public String getConditionsString(String tableName, String aliasTableName) { String condition = properties.get(ActiveRecordConstants.key_conditions_sql); if (condition != null) { if (condition.toLowerCase().indexOf((tableName+".").toLowerCase()) != -1 && !tableName.equalsIgnoreCase(aliasTableName)) { condition = StringUtil.replace(condition, tableName, aliasTableName); } } return condition; } /** * Sets the property map. */ public void setProperties(Map<String, String> properties) { if (properties == null || properties.size() == 0) return; this.properties = properties; mapping = properties.get(ActiveRecordConstants.key_mapping); targetModel = properties.get(ActiveRecordConstants.key_model); } /** * Returns a representation of the relation. * * @return relation key string */ public String getRelationKey() { return key; } /** * Sets relation key. * * @param key relation key string */ public void setRelationKey(String key) { this.key = key; } /** * Checks if the value of cascade property is none. * @return true if the value is none */ public boolean allowCascadeNone() { return allowCascade(CASCADE_NONE); } /** * Checks if the value of cascade property is nullify. * @return true if the value is nullify */ public boolean allowCascadeNullify() { return allowCascade(CASCADE_NULLIFY); } /** * Checks if the value of cascade property is delete. * @return true if the value is delete */ public boolean allowCascadeDelete() { return allowCascade(CASCADE_DELETE); } /** * Checks if the value of cascade property is simply_delete. * @return true if the value is delete */ public boolean allowCascadeSimplyDelete() { return allowCascade(CASCADE_SIMPLY_DELETE); } /** * Checks if a cascade type is allowed. * * @param cascadeType a cascade type string constant * @return true if the cascade type is allowed. */ public boolean allowCascade(String cascadeType) { String cascade = properties.get(ActiveRecordConstants.key_cascade); if (cascade == null) cascade = CASCADE_NONE; return (cascadeType.equalsIgnoreCase(cascade))?true:false; } public String toString() { String separator = "; "; StringBuilder sb = new StringBuilder(); sb.append("ownerClass: " + ownerClass.getName()).append(separator); sb.append("relation type: " + type).append(separator); sb.append("associationId: " + associationId).append(separator); sb.append("mapping: " + mapping).append(separator); sb.append("properties: " + properties).append(separator); sb.append("targetModel: " + targetModel).append(separator); sb.append("targetClass: " + getTargetClass()); return sb.toString(); } public static final String BELONGS_TO_TYPE = "belongs_to"; public static final String HAS_ONE_TYPE = "has_one"; public static final String HAS_MANY_TYPE = "has_many"; public static final String HAS_MANY_THROUGH_TYPE = "has_many_through"; /** * Cascade key to indicate no cascade effect. This is the default case. */ public static final String CASCADE_NONE = "none";//default /** * Cascade key to indicate the nullifying of the foreign key field in its * child record(s). */ public static final String CASCADE_NULLIFY = "nullify"; /** * Cascade key to indicate the delete of child record(s). This cascade * type will trigger actions caused by the removal of the child record * such as counter decrement in its parent record. */ public static final String CASCADE_DELETE = "delete"; /** * Simply delete the children without triggering any actions caused by * the removal of the child record such as counter decrement in its parent * record. */ public static final String CASCADE_SIMPLY_DELETE = "simply_delete"; /** * Specifies reverse relation name. */ public static final String REVERSE_RELATION = "reverse"; protected Class<? extends ActiveRecord> ownerClass; protected String type; protected String associationId; protected Class<? extends ActiveRecord> targetClass; protected String ownerModel; protected String targetModel; protected String mapping; protected Map<String, String> properties = new HashMap<String, String>(); protected String key; }