001package co.codewizards.cloudstore.local; 002 003import java.util.Date; 004import java.util.HashMap; 005import java.util.Map; 006 007import javax.jdo.JDOHelper; 008import javax.jdo.JDOObjectNotFoundException; 009import javax.jdo.PersistenceManager; 010import javax.jdo.listener.DeleteLifecycleListener; 011import javax.jdo.listener.InstanceLifecycleEvent; 012import javax.jdo.listener.StoreLifecycleListener; 013 014import org.slf4j.Logger; 015import org.slf4j.LoggerFactory; 016 017import co.codewizards.cloudstore.core.repo.local.AbstractLocalRepoTransactionListener; 018import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction; 019import co.codewizards.cloudstore.local.persistence.AutoTrackChanged; 020import co.codewizards.cloudstore.local.persistence.AutoTrackLocalRevision; 021 022/** 023 * JDO lifecycle-listener updating the {@link AutoTrackChanged#getChanged() changed} and the 024 * {@link AutoTrackLocalRevision#getLocalRevision() localRevision} properties of persistence-capable 025 * objects. 026 * <p> 027 * Whenever an object is written to the datastore, said properties are updated, if the appropriate 028 * interfaces are implemented by the persistence-capable object. 029 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co 030 */ 031public class AutoTrackLifecycleListener extends AbstractLocalRepoTransactionListener implements StoreLifecycleListener, DeleteLifecycleListener { 032 private static final Logger logger = LoggerFactory.getLogger(AutoTrackLifecycleListener.class); 033 034 private final Map<Object, Date> oid2LastChanged = new HashMap<>(); 035 private boolean defer; 036 037 @Override 038 public LocalRepoTransactionImpl getTransaction() { 039 return (LocalRepoTransactionImpl) super.getTransaction(); 040 } 041 042 @Override 043 protected LocalRepoTransactionImpl getTransactionOrFail() { 044 return (LocalRepoTransactionImpl) super.getTransactionOrFail(); 045 } 046 047 @Override 048 public void setTransaction(final LocalRepoTransaction transaction) { 049 if (! (transaction instanceof LocalRepoTransactionImpl)) 050 throw new IllegalArgumentException("transaction is not an instance of LocalRepoTransactionImpl!"); 051 052 super.setTransaction(transaction); 053 } 054 055 @Override 056 public void preStore(final InstanceLifecycleEvent event) { 057 // It seems, this method is always invoked whenever something is about to be written 058 // into the database - no matter, if it's a new object being persisted, a detached 059 // object being attached or a persistent object having been modified and being flushed. 060 // Therefore, we do not need AttachLifecycleListener and DirtyLifecycleListener. 061 // Marco :-) 062 onWrite(event.getPersistentInstance()); 063 } 064 065 @Override 066 public void postStore(final InstanceLifecycleEvent event) { } 067 068 @Override 069 public void preDelete(final InstanceLifecycleEvent event) { 070 // We want to ensure that the revision is incremented, even if we do not have any remote repository connected 071 // (and thus no DeleteModification being created). 072 getTransactionOrFail().getLocalRevision(); 073 074 final Object oid = JDOHelper.getObjectId(event.getPersistentInstance()); 075 oid2LastChanged.remove(oid); 076 } 077 078 @Override 079 public void postDelete(final InstanceLifecycleEvent event) { } 080 081 private void onWrite(final Object pc) { 082 // We always obtain the localRevision - no matter, if the current write operation is on 083 // an object implementing AutoTrackLocalRevision, because this causes incrementing of the 084 // localRevision in the database (once per transaction). 085 final long localRevision = getTransactionOrFail().getLocalRevision(); 086 087 final Date changed = new Date(); 088 final Object oid = JDOHelper.getObjectId(pc); 089 if (!defer && oid != null) { // there is no OID, yet, if the object is NEW (not yet persisted). 090 final Date oldLastChanged = oid2LastChanged.get(oid); 091 oid2LastChanged.put(oid, changed); // always keep the newest changed-timestamp. 092 093 if (oldLastChanged != null) { 094 logger.debug("onWrite: skipping (already processed in this transaction): {}", pc); 095 return; // already processed in this transaction. 096 } 097 } 098 099 if (pc instanceof AutoTrackChanged) { 100 logger.debug("onWrite: setChanged({}) for {}", changed, pc); 101 final AutoTrackChanged entity = (AutoTrackChanged) pc; 102 entity.setChanged(changed); 103 } 104 if (pc instanceof AutoTrackLocalRevision) { 105 logger.debug("onWrite: setLocalRevision({}) for {}", localRevision, pc); 106 final AutoTrackLocalRevision entity = (AutoTrackLocalRevision) pc; 107 entity.setLocalRevision(localRevision); 108 } 109 } 110 111 /** 112 * Notifies this instance about the {@linkplain #getTransaction() transaction} being begun. 113 * @see #onCommit() 114 * @see #onRollback() 115 */ 116 @Override 117 public void onBegin() { 118 defer = true; 119 getTransactionOrFail().getPersistenceManager().addInstanceLifecycleListener(this, (Class[]) null); 120 } 121 122 /** 123 * Notifies this instance about the {@linkplain #getTransaction() transaction} being committed. 124 * @see #onBegin() 125 * @see #onRollback() 126 */ 127 @Override 128 public void onCommit() { 129 defer = false; 130 final long start = System.currentTimeMillis(); 131 final PersistenceManager pm = getTransactionOrFail().getPersistenceManager(); 132 for (final Map.Entry<Object, Date> me : oid2LastChanged.entrySet()) { 133 try { 134 final Object pc = pm.getObjectById(me.getKey()); 135 if (pc instanceof AutoTrackChanged) { 136 final Date changed = me.getValue(); 137 logger.debug("onCommit: setChanged({}) for {}", changed, pc); 138 final AutoTrackChanged entity = (AutoTrackChanged) pc; 139 entity.setChanged(changed); 140 } 141 } catch (final JDOObjectNotFoundException x) { 142 logger.warn("onCommit: " + x, x); 143 } 144 } 145 final int oid2LastChangedSize = oid2LastChanged.size(); 146 oid2LastChanged.clear(); 147 148 final long duration = System.currentTimeMillis() - start; 149 if (duration >= 500) 150 logger.info("onCommit: Deferred operations took {} ms for {} entities.", duration, oid2LastChangedSize); 151 else 152 logger.debug("onCommit: Deferred operations took {} ms for {} entities.", duration, oid2LastChangedSize); 153 } 154 155 /** 156 * Notifies this instance about the {@linkplain #getTransaction() transaction} being rolled back. 157 * @see #onBegin() 158 * @see #onCommit() 159 */ 160 @Override 161 public void onRollback() { 162 defer = false; 163 oid2LastChanged.clear(); 164 } 165}