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}