/**
* Copyright (c) 2011-2012, James Zhan 詹波 (jfinal@126.com).
*
* 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 com.jfinal.plugin.activerecord;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
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 java.util.Map.Entry;
import com.jfinal.plugin.activerecord.cache.ICache;
import static com.jfinal.plugin.activerecord.DbKit.NULL_PARA_ARRAY;
/**
* Model
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public abstract class Model<M extends Model> implements Serializable {
private static final long serialVersionUID = -4890964905769110400L;
/**
* Attributes of this model
*/
private Map<String, Object> attrs = DbKit.mapFactory.getAttrsMap(); // new HashMap<String, Object>();
/**
* Flag of column has been modified. update need this flag
*/
private Set<String> modifyFlag;
private static final TableInfoMapping tableInfoMapping = TableInfoMapping.me();
private Set<String> getModifyFlag() {
if (modifyFlag == null)
modifyFlag = new HashSet<String>();
return modifyFlag;
}
/**
* Set attribute to model.
* @param attr the attribute name of the model
* @param value the value of the attribute
* @return this model
* @throws ActiveRecordException if the attribute is not exists of the model
*/
public M set(String attr, Object value) {
if (tableInfoMapping.getTableInfo(getClass()).hasColumnLabel(attr)) {
attrs.put(attr, value);
getModifyFlag().add(attr); // Add modify flag, update() need this flag.
}
else {
throw new ActiveRecordException("The attribute name is not exists: " + attr);
}
return (M)this;
}
/**
* Put key value pair to the model when the key is not attribute of the model.
*/
public M put(String key, Object value) {
attrs.put(key, value);
return (M)this;
}
/**
* Get attribute of any mysql type
*/
public <T> T get(String attr) {
return (T)attrs.get(attr);
}
/**
* Get attribute of any mysql type. Returns defaultValue if null.
*/
public <T> T get(String attr, Object defaultValue) {
Object result = attrs.get(attr);
return (T)(result != null ? result : defaultValue);
}
/**
* Get attribute of mysql type: varchar, char, enum, set, text, tinytext, mediumtext, longtext
*/
public String getStr(String attr) {
return (String)attrs.get(attr);
}
/**
* Get attribute of mysql type: int, integer, tinyint(n) n > 1, smallint, mediumint
*/
public Integer getInt(String attr) {
return (Integer)attrs.get(attr);
}
/**
* Get attribute of mysql type: bigint
*/
public Long getLong(String attr) {
return (Long)attrs.get(attr);
}
// java.util.Data never returned
// public java.util.Date getDate(String attr) {
// return attrs.get(attr);
//}
/**
* Get attribute of mysql type: date, year
*/
public java.sql.Date getDate(String attr) {
return (java.sql.Date)attrs.get(attr);
}
/**
* Get attribute of mysql type: time
*/
public java.sql.Time getTime(String attr) {
return (java.sql.Time)attrs.get(attr);
}
/**
* Get attribute of mysql type: timestamp, datetime
*/
public java.sql.Timestamp getTimestamp(String attr) {
return (java.sql.Timestamp)attrs.get(attr);
}
/**
* Get attribute of mysql type: real, double
*/
public Double getDouble(String attr) {
return (Double)attrs.get(attr);
}
/**
* Get attribute of mysql type: float
*/
public Float getFloat(String attr) {
return (Float)attrs.get(attr);
}
/**
* Get attribute of mysql type: bit, tinyint(1)
*/
public Boolean getBoolean(String attr) {
return (Boolean)attrs.get(attr);
}
/**
* Get attribute of mysql type: decimal, numeric
*/
public java.math.BigDecimal getBigDecimal(String attr) {
return (java.math.BigDecimal)attrs.get(attr);
}
/**
* Get attribute of mysql type: binary, varbinary, tinyblob, blob, mediumblob, longblob
*/
public byte[] getBytes(String attr) {
return (byte[])attrs.get(attr);
}
/**
* Get attribute of any type that extends from Number
*/
public Number getNumber(String attr) {
return (Number)attrs.get(attr);
}
/**
* Paginate.
* @param pageNumber the page number
* @param pageSize the page size
* @param select the select part of the sql statement
* @param sqlExceptSelect the sql statement excluded select part
* @param paras the parameters of sql
* @return Page
*/
public Page<M> paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) {
if (pageNumber < 1 || pageSize < 1)
throw new ActiveRecordException("pageNumber and pageSize must be more than 0");
if (DbKit.dialect.isTakeOverModelPaginate())
return DbKit.dialect.takeOverModelPaginate(getClass(), pageNumber, pageSize, select, sqlExceptSelect, paras);
Connection conn = null;
try {
conn = DbKit.getConnection();
long totalRow = 0;
int totalPage = 0;
List result = Db.query(conn, "select count(*) " + DbKit.replaceFormatSqlOrderBy(sqlExceptSelect), paras);
int size = result.size();
if (size == 1)
totalRow = ((Number)result.get(0)).longValue(); // totalRow = (Long)result.get(0);
else if (size > 1)
totalRow = result.size();
else
return new Page<M>(new ArrayList<M>(0), pageNumber, pageSize, 0, 0); // totalRow = 0;
totalPage = (int) (totalRow / pageSize);
if (totalRow % pageSize != 0) {
totalPage++;
}
// --------
StringBuilder sql = new StringBuilder();
DbKit.dialect.forPaginate(sql, pageNumber, pageSize, select, sqlExceptSelect);
List<M> list = find(conn, sql.toString(), paras);
return new Page<M>(list, pageNumber, pageSize, totalPage, (int)totalRow);
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
DbKit.close(conn);
}
}
/**
* @see #paginate(int, int, String, String, Object...)
*/
public Page<M> paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect) {
return paginate(pageNumber, pageSize, select, sqlExceptSelect, NULL_PARA_ARRAY);
}
/**
* Return attribute Map.
* <p>
* Danger! The update method will ignore the attribute if you change it directly.
* You must use set method to change attribute that update method can handle it.
*/
protected Map<String, Object> getAttrs() {
return attrs;
}
/**
* Return attribute Set.
*/
public Set<Entry<String, Object>> getAttrsEntrySet() {
return attrs.entrySet();
}
/**
* Save model.
*/
public boolean save() {
TableInfo tableInfo = tableInfoMapping.getTableInfo(getClass());
StringBuilder sql = new StringBuilder();
List<Object> paras = new ArrayList<Object>();
DbKit.dialect.forModelSave(tableInfo, attrs, sql, paras);
// if (paras.size() == 0) return false; // The sql "insert into tableName() values()" works fine, so delete this line
// --------
Connection conn = null;
PreparedStatement pst = null;
int result = 0;
try {
conn = DbKit.getConnection();
boolean isSupportAutoIncrementKey = DbKit.dialect.isSupportAutoIncrementKey();
if (isSupportAutoIncrementKey)
pst = conn.prepareStatement(sql.toString(), Statement.RETURN_GENERATED_KEYS);
else
pst = conn.prepareStatement(sql.toString());
for (int i=0, size=paras.size(); i<size; i++) {
pst.setObject(i + 1, paras.get(i));
}
result = pst.executeUpdate();
if (isSupportAutoIncrementKey)
getGeneratedKey(pst, tableInfo); // getGeneratedKey(pst, tableInfo.getPrimaryKey());
getModifyFlag().clear();
return result >= 1;
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
DbKit.close(pst, conn);
}
}
/**
* Get id after save method.
*/
private void getGeneratedKey(PreparedStatement pst, TableInfo tableInfo) throws SQLException {
String pKey = tableInfo.getPrimaryKey();
if (get(pKey) == null) {
ResultSet rs = pst.getGeneratedKeys();
if (rs.next()) {
Class colType = tableInfo.getColType(pKey);
if (colType == Integer.class || colType == int.class)
set(pKey, rs.getInt(1));
else if (colType == Long.class || colType == long.class)
set(pKey, rs.getLong(1));
else
set(pKey, rs.getObject(1)); // It returns Long object for int colType
rs.close();
}
}
}
/**
* Delete model.
*/
public boolean delete() {
TableInfo tInfo = tableInfoMapping.getTableInfo(getClass());
String pKey = tInfo.getPrimaryKey();
Object id = attrs.get(pKey);
if (id == null)
throw new ActiveRecordException("You can't delete model whitout id.");
return deleteById(tInfo, id);
}
/**
* Delete model by id.
* @param id the id value of the model
* @return true if delete succeed otherwise false
*/
public boolean deleteById(Object id) {
if (id == null)
throw new IllegalArgumentException("id can not be null");
TableInfo tInfo = tableInfoMapping.getTableInfo(getClass());
return deleteById(tInfo, id);
}
private boolean deleteById(TableInfo tInfo, Object id) {
String sql = DbKit.dialect.forModelDeleteById(tInfo);
return Db.update(sql, id) >= 1;
}
/**
* Update model.
*/
public boolean update() {
if (getModifyFlag().isEmpty())
return false;
TableInfo tableInfo = tableInfoMapping.getTableInfo(getClass());
String pKey = tableInfo.getPrimaryKey();
Object id = attrs.get(pKey);
if (id == null)
throw new ActiveRecordException("You can't update model whitout Primary Key.");
StringBuilder sql = new StringBuilder();
List<Object> paras = new ArrayList<Object>();
DbKit.dialect.forModelUpdate(tableInfo, attrs, getModifyFlag(), pKey, id, sql, paras);
if (paras.size() <= 1) { // Needn't update
return false;
}
// --------
Connection conn = null;
try {
conn = DbKit.getConnection();
int result = Db.update(conn, sql.toString(), paras.toArray());
if (result >= 1) {
getModifyFlag().clear();
return true;
}
return false;
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
DbKit.close(conn);
}
}
/**
* Find model.
*/
private List<M> find(Connection conn, String sql, Object... paras) throws Exception {
Class<? extends Model> modelClass = getClass();
if (DbKit.devMode)
checkTableName(modelClass, sql);
PreparedStatement pst = conn.prepareStatement(sql);
for (int i=0; i<paras.length; i++) {
pst.setObject(i + 1, paras[i]);
}
ResultSet rs = pst.executeQuery();
List<M> result = ModelBuilder.build(rs, modelClass);
DbKit.closeQuietly(rs, pst);
return result;
}
/**
* Find model.
* @param sql an SQL statement that may contain one or more '?' IN parameter placeholders
* @param paras the parameters of sql
* @return the list of Model
*/
public List<M> find(String sql, Object... paras) {
Connection conn = null;
try {
conn = DbKit.getConnection();
return find(conn, sql, paras);
} catch (Exception e) {
throw new ActiveRecordException(e);
} finally {
DbKit.close(conn);
}
}
/**
* Check the table name. The table name must in sql.
*/
private void checkTableName(Class<? extends Model> modelClass, String sql) {
TableInfo tableInfo = tableInfoMapping.getTableInfo(modelClass);
if (! sql.toLowerCase().contains(tableInfo.getTableName().toLowerCase()))
throw new ActiveRecordException("The table name: " + tableInfo.getTableName() + " not in your sql.");
}
/**
* @see #find(String, Object...)
*/
public List<M> find(String sql) {
return find(sql, NULL_PARA_ARRAY);
}
/**
* Find first model. I recommend add "limit 1" in your sql.
* @param sql an SQL statement that may contain one or more '?' IN parameter placeholders
* @param paras the parameters of sql
* @return Model
*/
public M findFirst(String sql, Object... paras) {
List<M> result = find(sql, paras);
return result.size() > 0 ? result.get(0) : null;
}
/**
* @see #findFirst(String, Object...)
* @param sql an SQL statement
*/
public M findFirst(String sql) {
List<M> result = find(sql, NULL_PARA_ARRAY);
return result.size() > 0 ? result.get(0) : null;
}
/**
* Find model by id.
* @param id the id value of the model
*/
public M findById(Object id) {
return findById(id, "*");
}
/**
* Find model by id. Fetch the specific columns only.
* Example: User user = User.dao.findById(15, "name, age");
* @param id the id value of the model
* @param columns the specific columns separate with comma character ==> ","
*/
public M findById(Object id, String columns) {
TableInfo tInfo = tableInfoMapping.getTableInfo(getClass());
String sql = DbKit.dialect.forModelFindById(tInfo, columns);
List<M> result = find(sql, id);
return result.size() > 0 ? result.get(0) : null;
}
/**
* Set attributes with other model.
* @param model the Model
* @return this Model
*/
public M setAttrs(M model) {
return setAttrs(model.getAttrs());
}
/**
* Set attributes with Map.
* @param model the Model
* @return this Model
*/
public M setAttrs(Map<String, Object> attrs) {
for (Entry<String, Object> e : attrs.entrySet()) {
set(e.getKey(), e.getValue());
}
return (M)this;
}
/**
* Remove attribute of this model.
* @param attr the attribute name of the model
* @return this model
*/
public M remove(String attr) {
attrs.remove(attr);
getModifyFlag().remove(attr);
return (M)this;
}
/**
* Remove attributes of this model.
* @param attrs the attribute names of the model
* @return this model
*/
public M remove(String... attrs) {
if (attrs != null)
for (String a : attrs) {
this.attrs.remove(a);
this.getModifyFlag().remove(a);
}
return (M)this;
}
/**
* Remove attributes if it is null.
* @return this model
*/
public M removeNullValueAttrs() {
for (Iterator<Entry<String, Object>> it = attrs.entrySet().iterator(); it.hasNext();) {
Entry<String, Object> e = it.next();
if (e.getValue() == null) {
it.remove();
getModifyFlag().remove(e.getKey());
}
}
return (M)this;
}
/**
* Keep attributes of this model and remove other attributes.
* @param attrs the attribute names of the model
* @return this model
*/
public M keep(String... attrs) {
if (attrs != null && attrs.length > 0) {
Map<String, Object> newAttrs = new HashMap<String, Object>(attrs.length);
Set<String> newModifyFlag = new HashSet<String>();
for (String a : attrs) {
if (this.attrs.containsKey(a)) // prevent put null value to the newColumns
newAttrs.put(a, this.attrs.get(a));
if (this.getModifyFlag().contains(a))
newModifyFlag.add(a);
}
this.attrs = newAttrs;
this.modifyFlag = newModifyFlag;
}
else {
this.attrs.clear();
this.getModifyFlag().clear();
}
return (M)this;
}
/**
* Keep attribute of this model and remove other attributes.
* @param attrs the attribute names of the model
* @return this model
*/
public M keep(String attr) {
if (attrs.containsKey(attr)) { // prevent put null value to the newColumns
Object keepIt = attrs.get(attr);
boolean keepFlag = getModifyFlag().contains(attr);
attrs.clear();
getModifyFlag().clear();
attrs.put(attr, keepIt);
if (keepFlag)
getModifyFlag().add(attr);
}
else {
attrs.clear();
getModifyFlag().clear();
}
return (M)this;
}
/**
* Remove all attributes of this model.
* @return this model
*/
public M clear() {
attrs.clear();
getModifyFlag().clear();
return (M)this;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append(" {");
boolean first = true;
for (Entry<String, Object> e : attrs.entrySet()) {
if (first)
first = false;
else
sb.append(", ");
Object value = e.getValue();
if (value != null)
value = value.toString();
sb.append(e.getKey()).append(":").append(value);
}
sb.append("}");
return sb.toString();
}
public boolean equals(Object o) {
if (!(o instanceof Model))
return false;
if (o == this)
return true;
return this.attrs.equals(((Model)o).attrs);
}
public int hashCode() {
return (attrs == null ? 0 : attrs.hashCode()) ^ (getModifyFlag() == null ? 0 : getModifyFlag().hashCode());
}
/**
* Find model by cache.
* @see #find(String, Object...)
* @param cacheName the cache name
* @param key the key used to get date from cache
* @return the list of Model
*/
public List<M> findByCache(String cacheName, Object key, String sql, Object... paras) {
ICache cache = DbKit.getCache();
List<M> result = cache.get(cacheName, key);
if (result == null) {
result = find(sql, paras);
cache.put(cacheName, key, result);
}
return result;
}
/**
* @see #findByCache(String, Object, String, Object...)
*/
public List<M> findByCache(String cacheName, Object key, String sql) {
return findByCache(cacheName, key, sql, NULL_PARA_ARRAY);
}
/**
* Paginate by cache.
* @see #paginate(int, int, String, String, Object...)
* @param cacheName the cache name
* @param key the key used to get date from cache
* @return Page
*/
public Page<M> paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras) {
ICache cache = DbKit.getCache();
Page<M> result = cache.get(cacheName, key);
if (result == null) {
result = paginate(pageNumber, pageSize, select, sqlExceptSelect, paras);
cache.put(cacheName, key, result);
}
return result;
}
/**
* @see #paginateByCache(String, Object, int, int, String, String, Object...)
*/
public Page<M> paginateByCache(String cacheName, Object key, int pageNumber, int pageSize, String select, String sqlExceptSelect) {
return paginateByCache(cacheName, key, pageNumber, pageSize, select, sqlExceptSelect, NULL_PARA_ARRAY);
}
/**
* Return attribute names of this model.
*/
public String[] getAttrNames() {
Set<String> attrNameSet = attrs.keySet();
return attrNameSet.toArray(new String[attrNameSet.size()]);
}
/**
* Return attribute values of this model.
*/
public Object[] getAttrValues() {
java.util.Collection<Object> attrValueCollection = attrs.values();
return attrValueCollection.toArray(new Object[attrValueCollection.size()]);
}
/**
* Return json string of this model.
*/
public String toJson() {
return com.jfinal.util.JsonBuilder.toJson(attrs, 4);
}
}