package co.codewizards.cloudstore.local;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.listener.DeleteLifecycleListener;
import javax.jdo.listener.InstanceLifecycleEvent;
import javax.jdo.listener.StoreLifecycleListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.repo.local.AbstractLocalRepoTransactionListener;
import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction;
import co.codewizards.cloudstore.local.persistence.AutoTrackChanged;
import co.codewizards.cloudstore.local.persistence.AutoTrackLocalRevision;
/**
* JDO lifecycle-listener updating the {@link AutoTrackChanged#getChanged() changed} and the
* {@link AutoTrackLocalRevision#getLocalRevision() localRevision} properties of persistence-capable
* objects.
* <p>
* Whenever an object is written to the datastore, said properties are updated, if the appropriate
* interfaces are implemented by the persistence-capable object.
* @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
*/
public class AutoTrackLifecycleListener extends AbstractLocalRepoTransactionListener implements StoreLifecycleListener, DeleteLifecycleListener {
private static final Logger logger = LoggerFactory.getLogger(AutoTrackLifecycleListener.class);
private final Map<Object, Date> oid2LastChanged = new HashMap<>();
private boolean defer;
@Override
public LocalRepoTransactionImpl getTransaction() {
return (LocalRepoTransactionImpl) super.getTransaction();
}
@Override
protected LocalRepoTransactionImpl getTransactionOrFail() {
return (LocalRepoTransactionImpl) super.getTransactionOrFail();
}
@Override
public void setTransaction(final LocalRepoTransaction transaction) {
if (! (transaction instanceof LocalRepoTransactionImpl))
throw new IllegalArgumentException("transaction is not an instance of LocalRepoTransactionImpl!");
super.setTransaction(transaction);
}
@Override
public void preStore(final InstanceLifecycleEvent event) {
// It seems, this method is always invoked whenever something is about to be written
// into the database - no matter, if it's a new object being persisted, a detached
// object being attached or a persistent object having been modified and being flushed.
// Therefore, we do not need AttachLifecycleListener and DirtyLifecycleListener.
// Marco :-)
onWrite(event.getPersistentInstance());
}
@Override
public void postStore(final InstanceLifecycleEvent event) { }
@Override
public void preDelete(final InstanceLifecycleEvent event) {
// We want to ensure that the revision is incremented, even if we do not have any remote repository connected
// (and thus no DeleteModification being created).
getTransactionOrFail().getLocalRevision();
final Object oid = JDOHelper.getObjectId(event.getPersistentInstance());
oid2LastChanged.remove(oid);
}
@Override
public void postDelete(final InstanceLifecycleEvent event) { }
private void onWrite(final Object pc) {
// We always obtain the localRevision - no matter, if the current write operation is on
// an object implementing AutoTrackLocalRevision, because this causes incrementing of the
// localRevision in the database (once per transaction).
final long localRevision = getTransactionOrFail().getLocalRevision();
final Date changed = new Date();
final Object oid = JDOHelper.getObjectId(pc);
if (!defer && oid != null) { // there is no OID, yet, if the object is NEW (not yet persisted).
final Date oldLastChanged = oid2LastChanged.get(oid);
oid2LastChanged.put(oid, changed); // always keep the newest changed-timestamp.
if (oldLastChanged != null) {
logger.debug("onWrite: skipping (already processed in this transaction): {}", pc);
return; // already processed in this transaction.
}
}
if (pc instanceof AutoTrackChanged) {
logger.debug("onWrite: setChanged({}) for {}", changed, pc);
final AutoTrackChanged entity = (AutoTrackChanged) pc;
entity.setChanged(changed);
}
if (pc instanceof AutoTrackLocalRevision) {
logger.debug("onWrite: setLocalRevision({}) for {}", localRevision, pc);
final AutoTrackLocalRevision entity = (AutoTrackLocalRevision) pc;
entity.setLocalRevision(localRevision);
}
}
/**
* Notifies this instance about the {@linkplain #getTransaction() transaction} being begun.
* @see #onCommit()
* @see #onRollback()
*/
@Override
public void onBegin() {
defer = true;
getTransactionOrFail().getPersistenceManager().addInstanceLifecycleListener(this, (Class[]) null);
}
/**
* Notifies this instance about the {@linkplain #getTransaction() transaction} being committed.
* @see #onBegin()
* @see #onRollback()
*/
@Override
public void onCommit() {
defer = false;
final long start = System.currentTimeMillis();
final PersistenceManager pm = getTransactionOrFail().getPersistenceManager();
for (final Map.Entry<Object, Date> me : oid2LastChanged.entrySet()) {
try {
final Object pc = pm.getObjectById(me.getKey());
if (pc instanceof AutoTrackChanged) {
final Date changed = me.getValue();
logger.debug("onCommit: setChanged({}) for {}", changed, pc);
final AutoTrackChanged entity = (AutoTrackChanged) pc;
entity.setChanged(changed);
}
} catch (final JDOObjectNotFoundException x) {
logger.warn("onCommit: " + x, x);
}
}
final int oid2LastChangedSize = oid2LastChanged.size();
oid2LastChanged.clear();
final long duration = System.currentTimeMillis() - start;
if (duration >= 500)
logger.info("onCommit: Deferred operations took {} ms for {} entities.", duration, oid2LastChangedSize);
else
logger.debug("onCommit: Deferred operations took {} ms for {} entities.", duration, oid2LastChangedSize);
}
/**
* Notifies this instance about the {@linkplain #getTransaction() transaction} being rolled back.
* @see #onBegin()
* @see #onCommit()
*/
@Override
public void onRollback() {
defer = false;
oid2LastChanged.clear();
}
}