/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.opentides.persistence.interceptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Types;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.CallbackException;
import org.hibernate.collection.internal.PersistentBag;
import org.hibernate.type.Type;
import org.opentides.annotation.Synchronizable;
import org.opentides.bean.BaseEntity;
import org.opentides.bean.ChangeLog;
import org.opentides.bean.ChangedField;
import org.opentides.bean.ChangedRecord;
import org.opentides.bean.JoinTable;
import org.opentides.bean.SystemCodes;
import org.opentides.bean.user.BaseUser;
import org.opentides.bean.user.UserCredential;
import org.opentides.context.ApplicationContextProvider;
import org.opentides.job.NotifyDevices;
import org.opentides.util.CacheUtil;
import org.opentides.util.StringUtil;
import org.opentides.util.SyncUtil;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* This interceptor is used by mobile sync to process hibernate operations and
* save the corresponding SQL statement in the change log. The change log is
* sent to the mobile devices for sync of database operations.
*
* @author allantan
*
*/
public class SynchronizableInterceptor extends AuditLogInterceptor {
private static final long serialVersionUID = 5476081576394866928L;
private static final Logger _log = Logger
.getLogger(SynchronizableInterceptor.class);
private static boolean disableAuditLog = false;
protected Set<PersistentBag> insertCollection = Collections
.synchronizedSet(new HashSet<PersistentBag>());
protected Set<PersistentBag> updateCollection = Collections
.synchronizedSet(new HashSet<PersistentBag>());
protected Set<ChangedRecord> updateRecords = Collections
.synchronizedSet(new HashSet<ChangedRecord>());
@Override
public boolean onFlushDirty(Object entity, Serializable id,
Object[] currentState, Object[] previousState,
String[] propertyNames, Type[] types) throws CallbackException {
if (entity instanceof BaseEntity) {
List<ChangedField> changedFields = new ArrayList<ChangedField>();
List<String> fields = CacheUtil.getSynchronizableFields((BaseEntity)entity, entity.getClass());
for (int i = 0; i < propertyNames.length; i++) {
if (currentState[i] != previousState[i]) {
String fieldName = propertyNames[i];
boolean sync = false;
// sync only fields declared
for (String syncName:fields) {
if (syncName.startsWith(fieldName)) {
fieldName = syncName;
sync = true;
}
}
if (sync)
changedFields.add(new ChangedField(currentState[i],
previousState[i], fieldName, types[i]));
}
}
if (changedFields.size() > 0) {
synchronized(updateRecords) {
updateRecords.add(new ChangedRecord(entity, id, changedFields));
}
}
}
return false;
}
@Override
public void onCollectionRecreate(Object collection, Serializable key)
throws CallbackException {
if (isSynchronizable(collection)) {
synchronized (insertCollection) {
insertCollection.add((PersistentBag) collection);
}
}
}
@Override
public void onCollectionRemove(Object collection, Serializable key)
throws CallbackException {
if (isSynchronizable(collection)) {
PersistentBag entries = (PersistentBag) collection;
BaseEntity owner = (BaseEntity) entries.getOwner();
Class<?> clazz = owner.getClass();
Class<?> clazz2 = entries.get(0).getClass();
JoinTable join = CacheUtil.getJoinTableFields(owner, clazz2);
if (join != null) {
StringBuffer statementBuffer = new StringBuffer();
statementBuffer.append("delete from ")
.append(join.getTableName()).append(" where ")
.append(join.getColumn1()).append(" = ?")
.append(" and ").append(join.getColumn2())
.append(" = ?");
String stmt = statementBuffer.toString();
for (Object obj : entries) {
BaseEntity entity = (BaseEntity) obj;
this.saveLog(owner, ChangeLog.DELETE, "", stmt,
"[" + owner.getId() + "," + entity.getId() + "]");
}
}
}
}
@Override
public void onCollectionUpdate(Object collection, Serializable key)
throws CallbackException {
if (isSynchronizable(collection)) {
synchronized (updateCollection) {
updateCollection.add((PersistentBag) collection);
}
}
}
/*
* (non-Javadoc)
*
* @see
* org.opentides.persistence.interceptor.AuditLogInterceptor#postFlush(java
* .util.Iterator)
*/
@Override
public void postFlush(Iterator iterator) throws CallbackException {
try {
// we may now delete the parent
synchronized (deletes) {
for (BaseEntity entity : deletes) {
if (isEntitySynchronizable(entity)) {
String deleteStmt = SyncUtil
.buildDeleteStatement(entity);
this.saveLog(entity, ChangeLog.DELETE, "", deleteStmt,
"["+entity.getId().toString()+"]");
}
}
}
// insert the parent first
synchronized (inserts) {
for (BaseEntity entity : inserts) {
if (isEntitySynchronizable(entity)) {
Method m = CacheUtil.getInsertMethod(entity.getClass());
if (m == null) {
String[] insertStmt = SyncUtil
.buildInsertStatement(entity);
this.saveLog(entity, ChangeLog.INSERT, "",
insertStmt[0], insertStmt[1]);
} else {
List<String[]> stmts = (List<String[]>) m.invoke(entity);
for (String[] stmt : stmts)
this.saveLog(entity, ChangeLog.INSERT, "",
stmt[0], stmt[1]);
}
}
}
}
// then insert child records
synchronized (insertCollection) {
for (PersistentBag collection : insertCollection) {
if (collection.size() > 0) {
BaseEntity owner = (BaseEntity) collection.getOwner();
JoinTable join = CacheUtil.getJoinTableFields(owner,
collection.get(0).getClass());
if (join != null) {
StringBuffer statementBuffer = new StringBuffer();
statementBuffer.append("insert into ")
.append(join.getTableName()).append(" (")
.append(join.getColumn1()).append(",")
.append(join.getColumn2())
.append(") VALUES (?,?)");
String stmt = statementBuffer.toString();
for (Object obj : collection) {
BaseEntity entity = (BaseEntity) obj;
this.saveLog(
owner,
ChangeLog.INSERT,
"",
stmt,
"[" + owner.getId() + ","
+ entity.getId() + "]");
}
}
}
}
}
synchronized (updateRecords) {
for (ChangedRecord record:updateRecords) {
if (isEntitySynchronizable(record.getEntity())) {
BaseEntity entity = (BaseEntity) record.getEntity();
Method m = CacheUtil.getUpdateMethod(entity.getClass());
if (m == null) {
List<String> fields = new ArrayList<String>();
for (ChangedField field:record.getChangedFields()) {
fields.add(field.getPropertyName());
}
String[] updateStmt = SyncUtil
.buildUpdateStatement(entity, fields);
this.saveLog(entity, ChangeLog.UPDATE,
StringUtils.join(fields, ","), updateStmt[0],
updateStmt[1]);
} else {
List<String[]> stmts = (List<String[]>) m.invoke(entity);
for (String[] stmt : stmts)
this.saveLog(entity, ChangeLog.UPDATE, "",
stmt[0], stmt[1]);
}
}
}
}
synchronized (updateCollection) {
for (PersistentBag collection : updateCollection) {
if (collection.size() > 0) {
BaseEntity owner = (BaseEntity) collection.getOwner();
JoinTable join = CacheUtil.getJoinTableFields(owner,
collection.get(0).getClass());
if (join != null) {
// delete old record
this.saveLog(owner, ChangeLog.DELETE, "",
"delete from " + join.getTableName()
+ " where " + join.getColumn1()
+ " = ?", "[" + owner.getId() + "]");
// insert the new
StringBuffer statementBuffer = new StringBuffer();
statementBuffer.append("insert into ")
.append(join.getTableName()).append(" (")
.append(join.getColumn1()).append(",")
.append(join.getColumn2())
.append(") VALUES (?,?)");
String stmt = statementBuffer.toString();
for (Object obj : collection) {
BaseEntity entity = (BaseEntity) obj;
this.saveLog(
owner,
ChangeLog.INSERT,
"",
stmt,
"[" + owner.getId() + ","
+ entity.getId() + "]");
}
}
}
}
}
// should we record auditLog from superclass?
if (!disableAuditLog)
super.postFlush(iterator);
} catch (Throwable e) {
_log.error(e, e);
} finally {
synchronized (inserts) {
inserts.clear();
}
synchronized (insertCollection) {
insertCollection.clear();
}
synchronized (updates) {
updates.clear();
}
synchronized (updateRecords) {
updateRecords.clear();
}
synchronized (updateCollection) {
updateCollection.clear();
}
synchronized (deletes) {
deletes.clear();
}
synchronized (oldies) {
oldies.clear();
}
}
}
/**
* Saves the change log into the database.
*
* @param shortMessage
* @param message
* @param entity
*/
public void saveLog(BaseEntity entity, int action, String updateFields,
String sqlCommand, String param) {
JdbcTemplate jTemplate = connectDb(entity.getDbName());
try {
String dateStr = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
.format(new Date());
String changeLogSql = "INSERT INTO CHANGE_LOG" + " (`CREATEDATE`, "
+ " `VERSION`, " + " `ACTION`, " + " `ENTITY_CLASS`, "
+ " `ENTITY_ID`, " + " `SQL_COMMAND`, " + " `PARAMS`, "
+ " `UPDATE_FIELDS`" + ") " + " VALUES (?,?,?,?,?,?,?,?); ";
Object[] params = new Object[] { dateStr, 0, action,
entity.getClass().getName(),
(entity.getId() == null) ? 0 : entity.getId(), sqlCommand,
param, updateFields };
int[] types = new int[] { Types.VARCHAR, Types.BIGINT,
Types.INTEGER, Types.VARCHAR, Types.BIGINT, Types.BLOB,
Types.BLOB, Types.VARCHAR };
jTemplate.update(changeLogSql, params, types);
NotifyDevices.notifySync("/" + entity.getDbName() + "/*");
} catch (Exception ex) {
_log.error("Failed to save change log on ["
+ entity.getClass().getSimpleName() + "]", ex);
}
}
/**
* @param disableAuditLog
* the disableAuditLog to set
*/
public static void setDisableAuditLog(boolean disableAuditLog) {
SynchronizableInterceptor.disableAuditLog = disableAuditLog;
}
public JdbcTemplate connectDb(String schemaName) {
// Get jdbc template
JdbcTemplate jTemplate = (JdbcTemplate) ApplicationContextProvider
.getApplicationContext().getBean("jdbcTemplate");
if (!StringUtil.isEmpty(schemaName))
jTemplate.execute("USE " + schemaName);
return jTemplate;
}
/**
* Private helper that checks if the given collection should be
* synchronized.
*
* @param collection
* @return
*/
protected boolean isSynchronizable(Object collection) {
if (collection instanceof PersistentBag) {
PersistentBag entries = (PersistentBag) collection;
if (entries.size() > 0) {
BaseEntity owner = (BaseEntity) entries.getOwner();
Class<?> clazz = owner.getClass();
Class<?> clazz2 = entries.get(0).getClass();
if (clazz.isAnnotationPresent(Synchronizable.class)
&& clazz2.isAnnotationPresent(Synchronizable.class)) {
return true;
}
}
}
return false;
}
/**
* Private helper that checks if the given fields should be synchronized.
*
* @param entity
* @return
*/
protected boolean isEntitySynchronizable(Object entity) {
return (entity.getClass().isAnnotationPresent(Synchronizable.class)
|| (entity instanceof SystemCodes)
|| (entity instanceof UserCredential) || (entity instanceof BaseUser));
}
}