/*
* Copyright (C) Tony Green, Litepal Framework Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.litepal;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.litepal.crud.model.AssociationsInfo;
import org.litepal.exceptions.DatabaseGenerateException;
import org.litepal.parser.LitePalAttr;
import org.litepal.tablemanager.model.AssociationsModel;
import org.litepal.tablemanager.model.TableModel;
import org.litepal.tablemanager.typechange.BooleanOrm;
import org.litepal.tablemanager.typechange.DateOrm;
import org.litepal.tablemanager.typechange.DecimalOrm;
import org.litepal.tablemanager.typechange.NumericOrm;
import org.litepal.tablemanager.typechange.OrmChange;
import org.litepal.tablemanager.typechange.TextOrm;
import org.litepal.util.BaseUtility;
import org.litepal.util.Const;
import org.litepal.util.DBUtility;
/**
* Base class of all the LitePal components. If each component need to
* interactive with other components or they have some same logic with duplicate
* codes, LitePalBase may be the solution.
*
* @author Tony Green
* @since 1.1
*/
public abstract class LitePalBase {
public static final String TAG = "LitePalBase";
/**
* Action to get associations.
*/
private static final int GET_ASSOCIATIONS_ACTION = 1;
/**
* Action to get association info.
*/
private static final int GET_ASSOCIATION_INFO_ACTION = 2;
/**
* All the supporting mapping types currently in the array.
*/
private OrmChange[] typeChangeRules = { new NumericOrm(), new TextOrm(), new BooleanOrm(),
new DecimalOrm(), new DateOrm() };
/**
* The collection contains all association models.
*/
private Collection<AssociationsModel> mAssociationModels;
/**
* The collection contains all association info.
*/
private Collection<AssociationsInfo> mAssociationInfos;
/**
* This method is used to get the table model by the class name passed
* in.The principle to generate table model is that each field in the class
* with private modifier and has a type among int/Integer, long/Long,
* short/Short, float/Float, double/Double, char/Character, boolean/Boolean
* or String, would generate a column with same name as corresponding field.
* If users don't want some of the fields map a column, declare them as
* protected or default.
*
* @param className
* The full name of the class to map in database.
* @return A table model with table name, class name and the map of column
* name and column type.
*/
protected TableModel getTableModel(String className) {
String tableName = DBUtility.getTableNameByClassName(className);
TableModel tableModel = new TableModel();
tableModel.setTableName(tableName);
tableModel.setClassName(className);
List<Field> supportedFields = getSupportedFields(className);
for (Field field : supportedFields) {
String fieldName = field.getName();
Class<?> fieldTypeClass = field.getType();
String fieldType = fieldTypeClass.getName();
String columnName = null;
String columnType = null;
for (OrmChange ormChange : typeChangeRules) {
String[] relations = ormChange.object2Relation(className, fieldName, fieldType);
if (relations != null) {
columnName = relations[0];
columnType = relations[1];
tableModel.addColumn(columnName, columnType);
break;
}
}
}
return tableModel;
}
/**
* This method is used to get association models depends on the given class
* name list.
*
* @param classNames
* The names of the classes that want to get their associations.
* @return Collection of association models.
*/
protected Collection<AssociationsModel> getAssociations(List<String> classNames) {
if (mAssociationModels == null) {
mAssociationModels = new HashSet<AssociationsModel>();
}
mAssociationModels.clear();
for (String className : classNames) {
analyzeClassFields(className, GET_ASSOCIATIONS_ACTION);
}
return mAssociationModels;
}
/**
* Get the association info model by the class name.
*
* @param className
* The class name to introspection.
* @return Collection of association info.
*/
protected Collection<AssociationsInfo> getAssociationInfo(String className) {
if (mAssociationInfos == null) {
mAssociationInfos = new HashSet<AssociationsInfo>();
}
mAssociationInfos.clear();
analyzeClassFields(className, GET_ASSOCIATION_INFO_ACTION);
return mAssociationInfos;
}
/**
* Find all the fields in the class. But not each field is supported to add
* a column to the table. Only the basic data types and String are
* supported. This method will intercept all the types which are not
* supported and return a new list of supported fields.
*
* @param className
* The full name of the class.
* @return A list of supported fields
*/
protected List<Field> getSupportedFields(String className) {
List<Field> supportedFields = new ArrayList<Field>();
Class<?> dynamicClass = null;
try {
dynamicClass = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new DatabaseGenerateException(DatabaseGenerateException.CLASS_NOT_FOUND + className);
}
Field[] fields = dynamicClass.getDeclaredFields();
for (Field field : fields) {
int modifiers = field.getModifiers();
if (Modifier.isPrivate(modifiers) && !Modifier.isStatic(modifiers)) {
Class<?> fieldTypeClass = field.getType();
String fieldType = fieldTypeClass.getName();
if (BaseUtility.isFieldTypeSupported(fieldType)) {
supportedFields.add(field);
}
}
}
return supportedFields;
}
/**
* If the field type implements from List or Set, regard it as a collection.
*
* @param fieldType
* The field type.
* @return True if the field type is collection, false otherwise.
*/
protected boolean isCollection(Class<?> fieldType) {
return isList(fieldType) || isSet(fieldType);
}
/**
* If the field type implements from List, regard it as a list.
*
* @param fieldType
* The field type.
* @return True if the field type is List, false otherwise.
*/
protected boolean isList(Class<?> fieldType) {
return List.class.isAssignableFrom(fieldType);
}
/**
* If the field type implements from Set, regard it as a set.
*
* @param fieldType
* The field type.
* @return True if the field type is Set, false otherwise.
*/
protected boolean isSet(Class<?> fieldType) {
return Set.class.isAssignableFrom(fieldType);
}
/**
* Judge the passed in column is an id column or not. The column named id or
* _id will be considered as id column.
*
* @param columnName
* The name of column.
* @return Return true if it's id column, otherwise return false.
*/
protected boolean isIdColumn(String columnName) {
return "_id".equalsIgnoreCase(columnName) || "id".equalsIgnoreCase(columnName);
}
/**
* If two tables are associated, one table have a foreign key column. The
* foreign key column name will be the associated table name with _id
* appended.
*
* @param associatedTableName
* The associated table name.
* @return The foreign key column name.
*/
protected String getForeignKeyColumnName(String associatedTableName) {
return BaseUtility.changeCase(associatedTableName + "_id");
}
/**
* Analyze the two parameters passed in. Return the first one of the two
* class names in alphabetical order as the one who holds foreign key. This
* is only going to work under one2one bidirectional association. When it's
* one2one unidirectional association, the foreign key column will be always
* on the side of the class which declares the association.
*
* @param className
* The first class name.
* @param associatedClassName
* The second class class.
* @return The first one of the two passed in parameters in alphabetical
* order.
*/
@Deprecated
protected String whoHoldsForeignKey(String className, String associatedClassName) {
String tableName = DBUtility.getTableNameByClassName(className);
String associatedTableName = DBUtility.getTableNameByClassName(associatedClassName);
if (tableName.compareTo(associatedTableName) < 0) {
return className;
} else {
return associatedClassName;
}
}
/**
* Introspection of the passed in class. Analyze the fields of current class
* and find out the associations of it.
*
* @param className
* The class name to introspection.
* @param action
* Between {@link LitePalBase#GET_ASSOCIATIONS_ACTION} and
* {@link LitePalBase#GET_ASSOCIATION_INFO_ACTION}
*/
private void analyzeClassFields(String className, int action) {
Class<?> dynamicClass = null;
try {
dynamicClass = Class.forName(className);
Field[] fields = dynamicClass.getDeclaredFields();
for (Field field : fields) {
if (isPrivateAndNonPrimitive(field)) {
oneToAnyConditions(className, field, action);
manyToAnyConditions(className, field, action);
}
}
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
throw new DatabaseGenerateException(DatabaseGenerateException.CLASS_NOT_FOUND + className);
}
}
/**
* Judge the field is a private non primitive field or not.
*
* @param field
* The field to judge.
* @return True if the field is <b>private</b> and <b>non primitive</b>,
* false otherwise.
*/
private boolean isPrivateAndNonPrimitive(Field field) {
return Modifier.isPrivate(field.getModifiers()) && !field.getType().isPrimitive();
}
/**
* Deals with one to any association conditions. e.g. Song and Album. An
* album have many songs, and a song belongs to one album. So if there's an
* Album model defined in Song with private modifier, and in Album there's a
* List or Set with generic type of Song and declared as private modifier,
* they are one2many association. If there's no List or Set defined in
* Album, they will become one2one associations. If there's also a Song
* model defined in Album with private modifier, maybe the album just have
* one song, they are one2one association too.
*
* When it's many2one association, it's easy to just simply add a foreign id
* column to the many side model's table. But when it comes to many2many
* association, it can not be done without intermediate join table in
* database. LitePal assumes that this join table's name is the
* concatenation of the two target table names in alphabetical order.
*
* @param className
* Source class name.
* @param field
* A field of source class.
* @param action
* Between {@link LitePalBase#GET_ASSOCIATIONS_ACTION} and
* {@link LitePalBase#GET_ASSOCIATION_INFO_ACTION}
*
* @throws ClassNotFoundException
*/
private void oneToAnyConditions(String className, Field field, int action) throws ClassNotFoundException {
Class<?> fieldTypeClass = field.getType();
// If the mapping list contains the class name
// defined in one class.
if (LitePalAttr.getInstance().getClassNames().contains(fieldTypeClass.getName())) {
Class<?> reverseDynamicClass = Class.forName(fieldTypeClass.getName());
Field[] reverseFields = reverseDynamicClass.getDeclaredFields();
// Look up if there's a reverse association
// definition in the reverse class.
boolean reverseAssociations = false;
// Begin to check the fields of the defined
// class.
for (int i = 0; i < reverseFields.length; i++) {
Field reverseField = reverseFields[i];
if (Modifier.isPrivate(reverseField.getModifiers())) {
Class<?> reverseFieldTypeClass = reverseField.getType();
// If there's the from class name in the
// defined class, they are one2one bidirectional
// associations.
if (className.equals(reverseFieldTypeClass.getName())) {
if (action == GET_ASSOCIATIONS_ACTION) {
addIntoAssociationModelCollection(className, fieldTypeClass.getName(),
fieldTypeClass.getName(), Const.Model.ONE_TO_ONE);
} else if (action == GET_ASSOCIATION_INFO_ACTION) {
addIntoAssociationInfoCollection(className, fieldTypeClass.getName(),
fieldTypeClass.getName(), field, reverseField, Const.Model.ONE_TO_ONE);
}
reverseAssociations = true;
}
// If there's the from class Set or List in
// the defined class, they are many2one bidirectional
// associations.
else if (isCollection(reverseFieldTypeClass)) {
String genericTypeName = getGenericTypeName(reverseField);
if (className.equals(genericTypeName)) {
if (action == GET_ASSOCIATIONS_ACTION) {
addIntoAssociationModelCollection(className, fieldTypeClass.getName(),
className, Const.Model.MANY_TO_ONE);
} else if (action == GET_ASSOCIATION_INFO_ACTION) {
addIntoAssociationInfoCollection(className, fieldTypeClass.getName(),
className, field, reverseField, Const.Model.MANY_TO_ONE);
}
reverseAssociations = true;
}
}
// If there's no from class in the defined class, they are
// one2one unidirectional associations.
if ((i == reverseFields.length - 1) && !reverseAssociations) {
if (action == GET_ASSOCIATIONS_ACTION) {
addIntoAssociationModelCollection(className, fieldTypeClass.getName(),
fieldTypeClass.getName(), Const.Model.ONE_TO_ONE);
} else if (action == GET_ASSOCIATION_INFO_ACTION) {
addIntoAssociationInfoCollection(className, fieldTypeClass.getName(),
fieldTypeClass.getName(), field, null, Const.Model.ONE_TO_ONE);
}
}
}
}
}
}
/**
* Deals with one to any association conditions. e.g. Song and Album. An
* album have many songs, and a song belongs to one album. So if there's an
* Album model defined in Song with private modifier, and in Album there's a
* List or Set with generic type of Song and declared as private modifier,
* they are one2many association. If there's no List or Set defined in
* Album, they will become one2one associations. If there's also a Song
* model defined in Album with private modifier, maybe the album just have
* one song, they are one2one association too.
*
* When it's many2one association, it's easy to just simply add a foreign id
* column to the many side model's table. But when it comes to many2many
* association, it can not be done without intermediate join table in
* database. LitePal assumes that this join table's name is the
* concatenation of the two target table names in alphabetical order.
*
* @param className
* Source class name.
* @param field
* A field of source class.
* @param action
* Between {@link LitePalBase#GET_ASSOCIATIONS_ACTION} and
* {@link LitePalBase#GET_ASSOCIATION_INFO_ACTION}
*
* @throws ClassNotFoundException
*/
private void manyToAnyConditions(String className, Field field, int action) throws ClassNotFoundException {
if (isCollection(field.getType())) {
String genericTypeName = getGenericTypeName(field);
// If the mapping list contains the genericTypeName, begin to check
// this genericTypeName class.
if (LitePalAttr.getInstance().getClassNames().contains(genericTypeName)) {
Class<?> reverseDynamicClass = Class.forName(genericTypeName);
Field[] reverseFields = reverseDynamicClass.getDeclaredFields();
// Look up if there's a reverse association
// definition in the reverse class.
boolean reverseAssociations = false;
for (int i = 0; i < reverseFields.length; i++) {
Field reverseField = reverseFields[i];
// Only map private fields
if (Modifier.isPrivate(reverseField.getModifiers())) {
Class<?> reverseFieldTypeClass = reverseField.getType();
// If there's a from class name defined in the reverse
// class, they are many2one bidirectional
// associations.
if (className.equals(reverseFieldTypeClass.getName())) {
if (action == GET_ASSOCIATIONS_ACTION) {
addIntoAssociationModelCollection(className, genericTypeName,
genericTypeName, Const.Model.MANY_TO_ONE);
} else if (action == GET_ASSOCIATION_INFO_ACTION) {
addIntoAssociationInfoCollection(className, genericTypeName, genericTypeName,
field, reverseField, Const.Model.MANY_TO_ONE);
}
reverseAssociations = true;
}
// If there's a List or Set contains from class name
// defined in the reverse class, they are many2many
// association.
else if (isCollection(reverseFieldTypeClass)) {
String reverseGenericTypeName = getGenericTypeName(reverseField);
if (className.equals(reverseGenericTypeName)) {
if (action == GET_ASSOCIATIONS_ACTION) {
addIntoAssociationModelCollection(className, genericTypeName, null,
Const.Model.MANY_TO_MANY);
} else if (action == GET_ASSOCIATION_INFO_ACTION) {
addIntoAssociationInfoCollection(className, genericTypeName, null, field,
reverseField, Const.Model.MANY_TO_MANY);
}
reverseAssociations = true;
}
}
// If there's no from class in the defined class, they
// are many2one unidirectional associations.
if ((i == reverseFields.length - 1) && !reverseAssociations) {
if (action == GET_ASSOCIATIONS_ACTION) {
addIntoAssociationModelCollection(className, genericTypeName,
genericTypeName, Const.Model.MANY_TO_ONE);
} else if (action == GET_ASSOCIATION_INFO_ACTION) {
addIntoAssociationInfoCollection(className, genericTypeName, genericTypeName,
field, null, Const.Model.MANY_TO_ONE);
}
}
}
}
}
}
}
/**
* Package a {@link AssociationsModel}, and add it into
* {@link #mAssociationModels} Collection.
*
* @param className
* The class name for {@link AssociationsModel}.
* @param associatedClassName
* The associated class name for {@link AssociationsModel}.
* @param classHoldsForeignKey
* The class which holds foreign key.
* @param associationType
* The association type for {@link AssociationsModel}.
*/
private void addIntoAssociationModelCollection(String className, String associatedClassName,
String classHoldsForeignKey, int associationType) {
AssociationsModel associationModel = new AssociationsModel();
associationModel.setTableName(DBUtility.getTableNameByClassName(className));
associationModel.setAssociatedTableName(DBUtility.getTableNameByClassName(associatedClassName));
associationModel.setTableHoldsForeignKey(DBUtility.getTableNameByClassName(classHoldsForeignKey));
associationModel.setAssociationType(associationType);
mAssociationModels.add(associationModel);
}
/**
* Package a {@link AssociationsInfo}, and add it into
* {@link #mAssociationInfos} Collection.
*
* @param selfClassName
* The class name of self model.
* @param associatedClassName
* The class name of the class which associated with self class.
* @param classHoldsForeignKey
* The class which holds foreign key.
* @param associateOtherModelFromSelf
* The field of self class to declare has association with other
* class.
* @param associateSelfFromOtherModel
* The field of the associated class to declare has association
* with self class.
* @param associationType
* The association type.
*/
private void addIntoAssociationInfoCollection(String selfClassName, String associatedClassName,
String classHoldsForeignKey, Field associateOtherModelFromSelf,
Field associateSelfFromOtherModel, int associationType) {
AssociationsInfo associationInfo = new AssociationsInfo();
associationInfo.setSelfClassName(selfClassName);
associationInfo.setAssociatedClassName(associatedClassName);
associationInfo.setClassHoldsForeignKey(classHoldsForeignKey);
associationInfo.setAssociateOtherModelFromSelf(associateOtherModelFromSelf);
associationInfo.setAssociateSelfFromOtherModel(associateSelfFromOtherModel);
associationInfo.setAssociationType(associationType);
mAssociationInfos.add(associationInfo);
}
/**
* Get the generic type name of List or Set. If there's no generic type of
* List or Set return null.
*
* @param field
* A generic type field.
* @return The name of generic type of List of Set.
*/
private String getGenericTypeName(Field field) {
Type genericType = field.getGenericType();
if (genericType != null) {
if (genericType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericType;
Class<?> genericArg = (Class<?>) parameterizedType.getActualTypeArguments()[0];
return genericArg.getName();
}
}
return null;
}
}