2012-03-13 15:48:26 -07:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
2011-12-21 08:44:08 -08:00
|
|
|
|
|
|
|
package org.mozilla.gecko.sync.repositories;
|
|
|
|
|
2012-02-23 08:14:05 -08:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Iterator;
|
2012-01-14 09:20:31 -08:00
|
|
|
import java.util.concurrent.ExecutorService;
|
|
|
|
import java.util.concurrent.Executors;
|
|
|
|
|
2012-02-15 22:05:52 -08:00
|
|
|
import org.mozilla.gecko.sync.Logger;
|
2011-12-21 08:44:08 -08:00
|
|
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
|
|
|
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
|
|
|
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
|
|
|
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
|
|
|
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
|
|
|
|
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
|
|
|
|
import org.mozilla.gecko.sync.repositories.domain.Record;
|
|
|
|
|
|
|
|
/**
|
2012-03-13 15:48:26 -07:00
|
|
|
* A <code>RepositorySession</code> is created and used thusly:
|
2011-12-21 08:44:08 -08:00
|
|
|
*
|
2012-03-13 15:48:26 -07:00
|
|
|
*<ul>
|
|
|
|
* <li>Construct, with a reference to its parent {@link Repository}, by calling
|
|
|
|
* {@link Repository#createSession(RepositorySessionCreationDelegate, android.content.Context)}.</li>
|
|
|
|
* <li>Populate with saved information by calling {@link #unbundle(RepositorySessionBundle)}.</li>
|
|
|
|
* <li>Begin a sync by calling {@link #begin(RepositorySessionBeginDelegate)}. <code>begin()</code>
|
|
|
|
* is an appropriate place to initialize expensive resources.</li>
|
|
|
|
* <li>Perform operations such as {@link #fetchSince(long, RepositorySessionFetchRecordsDelegate)} and
|
|
|
|
* {@link #store(Record)}.</li>
|
|
|
|
* <li>Finish by calling {@link #finish(RepositorySessionFinishDelegate)}, retrieving and storing
|
|
|
|
* the current bundle.</li>
|
|
|
|
*</ul>
|
2011-12-21 08:44:08 -08:00
|
|
|
*
|
2012-03-13 15:48:26 -07:00
|
|
|
* If <code>finish()</code> is not called, {@link #abort()} must be called. These calls must
|
|
|
|
* <em>always</em> be paired with <code>begin()</code>.
|
2011-12-21 08:44:08 -08:00
|
|
|
*
|
|
|
|
*/
|
|
|
|
public abstract class RepositorySession {
|
|
|
|
|
|
|
|
public enum SessionStatus {
|
|
|
|
UNSTARTED,
|
|
|
|
ACTIVE,
|
|
|
|
ABORTED,
|
|
|
|
DONE
|
|
|
|
}
|
|
|
|
|
|
|
|
private static final String LOG_TAG = "RepositorySession";
|
2012-02-03 13:09:29 -08:00
|
|
|
|
|
|
|
protected static void trace(String message) {
|
2012-02-15 22:05:52 -08:00
|
|
|
Logger.trace(LOG_TAG, message);
|
2012-02-03 13:09:29 -08:00
|
|
|
}
|
|
|
|
|
2012-03-05 20:53:14 -08:00
|
|
|
private SessionStatus status = SessionStatus.UNSTARTED;
|
2011-12-21 08:44:08 -08:00
|
|
|
protected Repository repository;
|
2012-01-14 09:20:31 -08:00
|
|
|
protected RepositorySessionStoreDelegate delegate;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A queue of Runnables which call out into delegates.
|
|
|
|
*/
|
|
|
|
protected ExecutorService delegateQueue = Executors.newSingleThreadExecutor();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A queue of Runnables which effect storing.
|
|
|
|
* This includes actual store work, and also the consequences of storeDone.
|
|
|
|
* This provides strict ordering.
|
|
|
|
*/
|
|
|
|
protected ExecutorService storeWorkQueue = Executors.newSingleThreadExecutor();
|
2011-12-21 08:44:08 -08:00
|
|
|
|
|
|
|
// The time that the last sync on this collection completed, in milliseconds since epoch.
|
|
|
|
public long lastSyncTimestamp;
|
|
|
|
|
|
|
|
public static long now() {
|
|
|
|
return System.currentTimeMillis();
|
|
|
|
}
|
|
|
|
|
|
|
|
public RepositorySession(Repository repository) {
|
|
|
|
this.repository = repository;
|
|
|
|
}
|
|
|
|
|
|
|
|
public abstract void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate);
|
|
|
|
public abstract void fetchSince(long timestamp, RepositorySessionFetchRecordsDelegate delegate);
|
2012-03-05 20:53:14 -08:00
|
|
|
public abstract void fetch(String[] guids, RepositorySessionFetchRecordsDelegate delegate) throws InactiveSessionException;
|
2011-12-21 08:44:08 -08:00
|
|
|
public abstract void fetchAll(RepositorySessionFetchRecordsDelegate delegate);
|
2012-01-14 09:20:31 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Override this if you wish to short-circuit a sync when you know --
|
|
|
|
* e.g., by inspecting the database or info/collections -- that no new
|
|
|
|
* data are available.
|
|
|
|
*
|
|
|
|
* @return true if a sync should proceed.
|
|
|
|
*/
|
|
|
|
public boolean dataAvailable() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Store operations proceed thusly:
|
|
|
|
*
|
|
|
|
* * Set a delegate
|
|
|
|
* * Store an arbitrary number of records. At any time the delegate can be
|
|
|
|
* notified of an error.
|
|
|
|
* * Call storeDone to notify the session that no more items are forthcoming.
|
|
|
|
* * The store delegate will be notified of error or completion.
|
|
|
|
*
|
|
|
|
* This arrangement of calls allows for batching at the session level.
|
|
|
|
*
|
|
|
|
* Store success calls are not guaranteed.
|
|
|
|
*/
|
|
|
|
public void setStoreDelegate(RepositorySessionStoreDelegate delegate) {
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "Setting store delegate to " + delegate);
|
2012-01-14 09:20:31 -08:00
|
|
|
this.delegate = delegate;
|
|
|
|
}
|
|
|
|
public abstract void store(Record record) throws NoStoreDelegateException;
|
|
|
|
|
|
|
|
public void storeDone() {
|
2012-02-15 22:05:53 -08:00
|
|
|
// Our default behavior will be to assume that the Runnable is
|
|
|
|
// executed as soon as all the stores synchronously finish, so
|
|
|
|
// our end timestamp can just be… now.
|
|
|
|
storeDone(now());
|
|
|
|
}
|
|
|
|
|
|
|
|
public void storeDone(final long end) {
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "Scheduling onStoreCompleted for after storing is done.");
|
2012-01-14 09:20:31 -08:00
|
|
|
Runnable command = new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
2012-02-15 22:05:53 -08:00
|
|
|
delegate.onStoreCompleted(end);
|
2012-01-14 09:20:31 -08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
storeWorkQueue.execute(command);
|
|
|
|
}
|
|
|
|
|
2011-12-21 08:44:08 -08:00
|
|
|
public abstract void wipe(RepositorySessionWipeDelegate delegate);
|
|
|
|
|
|
|
|
public void unbundle(RepositorySessionBundle bundle) {
|
|
|
|
this.lastSyncTimestamp = 0;
|
|
|
|
if (bundle == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (bundle.containsKey("timestamp")) {
|
|
|
|
try {
|
|
|
|
this.lastSyncTimestamp = bundle.getLong("timestamp");
|
|
|
|
} catch (Exception e) {
|
|
|
|
// Defaults to 0 above.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Synchronously perform the shared work of beginning. Throws on failure.
|
|
|
|
* @throws InvalidSessionTransitionException
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
protected void sharedBegin() throws InvalidSessionTransitionException {
|
2012-03-05 20:53:14 -08:00
|
|
|
Logger.debug(LOG_TAG, "Shared begin.");
|
|
|
|
if (delegateQueue.isShutdown()) {
|
2011-12-21 08:44:08 -08:00
|
|
|
throw new InvalidSessionTransitionException(null);
|
|
|
|
}
|
2012-03-05 20:53:14 -08:00
|
|
|
if (storeWorkQueue.isShutdown()) {
|
|
|
|
throw new InvalidSessionTransitionException(null);
|
|
|
|
}
|
|
|
|
this.transitionFrom(SessionStatus.UNSTARTED, SessionStatus.ACTIVE);
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
|
|
|
|
2012-03-13 15:48:26 -07:00
|
|
|
/**
|
|
|
|
* Start the session. This is an appropriate place to initialize
|
|
|
|
* data access components such as database handles.
|
|
|
|
*
|
|
|
|
* @param delegate
|
|
|
|
* @throws InvalidSessionTransitionException
|
|
|
|
*/
|
2012-03-05 20:53:14 -08:00
|
|
|
public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException {
|
|
|
|
sharedBegin();
|
|
|
|
delegate.deferredBeginDelegate(delegateQueue).onBeginSucceeded(this);
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
protected RepositorySessionBundle getBundle() {
|
|
|
|
return this.getBundle(null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Override this in your subclasses to return values to save between sessions.
|
|
|
|
* Note that RepositorySession automatically bumps the timestamp to the time
|
|
|
|
* the last sync began. If unbundled but not begun, this will be the same as the
|
|
|
|
* value in the input bundle.
|
|
|
|
*
|
|
|
|
* The Synchronizer most likely wants to bump the bundle timestamp to be a value
|
|
|
|
* return from a fetch call.
|
|
|
|
*/
|
|
|
|
protected RepositorySessionBundle getBundle(RepositorySessionBundle optional) {
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "RepositorySession.getBundle(optional).");
|
2011-12-21 08:44:08 -08:00
|
|
|
// Why don't we just persist the old bundle?
|
|
|
|
RepositorySessionBundle bundle = (optional == null) ? new RepositorySessionBundle() : optional;
|
|
|
|
bundle.put("timestamp", this.lastSyncTimestamp);
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "Setting bundle timestamp to " + this.lastSyncTimestamp);
|
2011-12-21 08:44:08 -08:00
|
|
|
return bundle;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Just like finish(), but doesn't do any work that should only be performed
|
|
|
|
* at the end of a successful sync, and can be called any time.
|
|
|
|
*/
|
|
|
|
public void abort(RepositorySessionFinishDelegate delegate) {
|
2012-03-05 20:53:14 -08:00
|
|
|
this.abort();
|
2012-01-14 09:20:31 -08:00
|
|
|
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
|
|
|
|
2012-03-13 15:48:26 -07:00
|
|
|
/**
|
|
|
|
* Abnormally terminate the repository session, freeing or closing
|
|
|
|
* any resources that were opened during the lifetime of the session.
|
|
|
|
*/
|
2012-03-05 20:53:14 -08:00
|
|
|
public void abort() {
|
|
|
|
// TODO: do something here.
|
|
|
|
this.setStatus(SessionStatus.ABORTED);
|
|
|
|
try {
|
2012-03-20 13:35:09 -07:00
|
|
|
storeWorkQueue.shutdownNow();
|
2012-03-05 20:53:14 -08:00
|
|
|
} catch (Exception e) {
|
|
|
|
Logger.error(LOG_TAG, "Caught exception shutting down store work queue.", e);
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
delegateQueue.shutdown();
|
|
|
|
} catch (Exception e) {
|
|
|
|
Logger.error(LOG_TAG, "Caught exception shutting down delegate queue.", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-13 15:48:26 -07:00
|
|
|
/**
|
|
|
|
* End the repository session, freeing or closing any resources
|
|
|
|
* that were opened during the lifetime of the session.
|
|
|
|
*
|
|
|
|
* @param delegate notified of success or failure.
|
|
|
|
* @throws InactiveSessionException
|
|
|
|
*/
|
2012-03-05 20:53:14 -08:00
|
|
|
public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
|
|
|
|
try {
|
|
|
|
this.transitionFrom(SessionStatus.ACTIVE, SessionStatus.DONE);
|
2012-01-14 09:20:31 -08:00
|
|
|
delegate.deferredFinishDelegate(delegateQueue).onFinishSucceeded(this, this.getBundle(null));
|
2012-03-05 20:53:14 -08:00
|
|
|
} catch (InvalidSessionTransitionException e) {
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.error(LOG_TAG, "Tried to finish() an unstarted or already finished session");
|
2012-03-05 20:53:14 -08:00
|
|
|
InactiveSessionException ex = new InactiveSessionException(null);
|
|
|
|
ex.initCause(e);
|
|
|
|
throw ex;
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
2012-03-05 20:53:14 -08:00
|
|
|
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.info(LOG_TAG, "Shutting down work queues.");
|
2012-03-05 20:53:14 -08:00
|
|
|
storeWorkQueue.shutdown();
|
|
|
|
delegateQueue.shutdown();
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
|
|
|
|
2012-03-05 20:53:14 -08:00
|
|
|
/**
|
|
|
|
* Run the provided command if we're active and our delegate queue
|
|
|
|
* is not shut down.
|
|
|
|
*/
|
|
|
|
protected synchronized void executeDelegateCommand(Runnable command)
|
|
|
|
throws InactiveSessionException {
|
|
|
|
if (!isActive() || delegateQueue.isShutdown()) {
|
|
|
|
throw new InactiveSessionException(null);
|
|
|
|
}
|
|
|
|
delegateQueue.execute(command);
|
|
|
|
}
|
|
|
|
|
|
|
|
public synchronized void ensureActive() throws InactiveSessionException {
|
|
|
|
if (!isActive()) {
|
|
|
|
throw new InactiveSessionException(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public synchronized boolean isActive() {
|
2011-12-21 08:44:08 -08:00
|
|
|
return status == SessionStatus.ACTIVE;
|
|
|
|
}
|
|
|
|
|
2012-03-05 20:53:14 -08:00
|
|
|
public synchronized SessionStatus getStatus() {
|
2012-02-21 13:29:38 -08:00
|
|
|
return status;
|
|
|
|
}
|
|
|
|
|
2012-03-05 20:53:14 -08:00
|
|
|
public synchronized void setStatus(SessionStatus status) {
|
2012-02-21 13:29:38 -08:00
|
|
|
this.status = status;
|
|
|
|
}
|
|
|
|
|
2012-03-05 20:53:14 -08:00
|
|
|
public synchronized void transitionFrom(SessionStatus from, SessionStatus to) throws InvalidSessionTransitionException {
|
|
|
|
if (from == null || this.status == from) {
|
|
|
|
Logger.trace(LOG_TAG, "Successfully transitioning from " + this.status + " to " + to);
|
|
|
|
|
|
|
|
this.status = to;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Logger.warn(LOG_TAG, "Wanted to transition from " + from + " but in state " + this.status);
|
|
|
|
throw new InvalidSessionTransitionException(null);
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|
2012-02-03 13:09:29 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Produce a record that is some combination of the remote and local records
|
|
|
|
* provided.
|
|
|
|
*
|
|
|
|
* The returned record must be produced without mutating either remoteRecord
|
|
|
|
* or localRecord. It is acceptable to return either remoteRecord or localRecord
|
|
|
|
* if no modifications are to be propagated.
|
|
|
|
*
|
|
|
|
* The returned record *should* have the local androidID and the remote GUID,
|
|
|
|
* and some optional merge of data from the two records.
|
|
|
|
*
|
|
|
|
* This method can be called with records that are identical, or differ in
|
|
|
|
* any regard.
|
|
|
|
*
|
|
|
|
* This method will not be called if:
|
|
|
|
*
|
|
|
|
* * either record is marked as deleted, or
|
|
|
|
* * there is no local mapping for a new remote record.
|
|
|
|
*
|
|
|
|
* Otherwise, it will be called precisely once.
|
|
|
|
*
|
|
|
|
* Side-effects (e.g., for transactional storage) can be hooked in here.
|
|
|
|
*
|
|
|
|
* @param remoteRecord
|
|
|
|
* The record retrieved from upstream, already adjusted for clock skew.
|
|
|
|
* @param localRecord
|
|
|
|
* The record retrieved from local storage.
|
|
|
|
* @param lastRemoteRetrieval
|
|
|
|
* The timestamp of the last retrieved set of remote records, adjusted for
|
|
|
|
* clock skew.
|
|
|
|
* @param lastLocalRetrieval
|
|
|
|
* The timestamp of the last retrieved set of local records.
|
|
|
|
* @return
|
|
|
|
* A Record instance to apply, or null to apply nothing.
|
|
|
|
*/
|
|
|
|
protected Record reconcileRecords(final Record remoteRecord,
|
|
|
|
final Record localRecord,
|
|
|
|
final long lastRemoteRetrieval,
|
|
|
|
final long lastLocalRetrieval) {
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid);
|
2012-02-03 13:09:29 -08:00
|
|
|
|
|
|
|
if (localRecord.equalPayloads(remoteRecord)) {
|
|
|
|
if (remoteRecord.lastModified > localRecord.lastModified) {
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "Records are equal. No record application needed.");
|
2012-02-03 13:09:29 -08:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Local wins.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Decide what to do based on:
|
|
|
|
// * Which of the two records is modified;
|
|
|
|
// * Whether they are equal or congruent;
|
|
|
|
// * The modified times of each record (interpreted through the lens of clock skew);
|
|
|
|
// * ...
|
|
|
|
boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified;
|
2012-02-23 08:14:05 -08:00
|
|
|
Logger.debug(LOG_TAG, "Local record is more recent? " + localIsMoreRecent);
|
2012-02-03 13:09:29 -08:00
|
|
|
Record donor = localIsMoreRecent ? localRecord : remoteRecord;
|
|
|
|
|
|
|
|
// Modify the local record to match the remote record's GUID and values.
|
|
|
|
// Preserve the local Android ID, and merge data where possible.
|
|
|
|
// It sure would be nice if copyWithIDs didn't give a shit about androidID, mm?
|
|
|
|
Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID);
|
|
|
|
|
|
|
|
// We don't want to upload the record if the remote record was
|
|
|
|
// applied without changes.
|
|
|
|
// This logic will become more complicated as reconciling becomes smarter.
|
|
|
|
if (!localIsMoreRecent) {
|
|
|
|
trackRecord(out);
|
|
|
|
}
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Depending on the RepositorySession implementation, track
|
|
|
|
* that a record — most likely a brand-new record that has been
|
|
|
|
* applied unmodified — should be tracked so as to not be uploaded
|
|
|
|
* redundantly.
|
|
|
|
*
|
|
|
|
* The default implementation does nothing.
|
|
|
|
*/
|
|
|
|
protected synchronized void trackRecord(Record record) {
|
|
|
|
}
|
2012-02-23 08:14:05 -08:00
|
|
|
|
|
|
|
protected synchronized void untrackRecord(Record record) {
|
|
|
|
}
|
2012-02-23 08:14:05 -08:00
|
|
|
|
|
|
|
// Ah, Java. You wretched creature.
|
|
|
|
public Iterator<String> getTrackedRecordIDs() {
|
|
|
|
return new ArrayList<String>().iterator();
|
|
|
|
}
|
2011-12-21 08:44:08 -08:00
|
|
|
}
|