/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2003 - 2007 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package com.funambol.sapisync;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Date;
import java.util.Enumeration;
import java.io.IOException;
import com.funambol.org.json.me.JSONArray;
import com.funambol.org.json.me.JSONException;
import com.funambol.org.json.me.JSONObject;
import com.funambol.sync.BasicSyncListener;
import com.funambol.sync.ItemStatus;
import com.funambol.sync.SyncAnchor;
import com.funambol.sync.SyncConfig;
import com.funambol.sync.SyncException;
import com.funambol.sync.SyncItem;
import com.funambol.sync.SyncListener;
import com.funambol.sync.SyncSource;
import com.funambol.sync.SyncManagerI;
import com.funambol.sapisync.source.JSONSyncItem;
import com.funambol.sapisync.source.util.HttpDownloader;
import com.funambol.sync.Filter;
import com.funambol.sync.SyncFilter;
import com.funambol.sync.DeviceConfigI;
import com.funambol.sync.NonBlockingSyncException;
import com.funambol.util.Log;
import com.funambol.util.StringUtil;
/**
* <code>SapiSyncManager</code> represents the synchronization engine performed
* via SAPI.
*/
public class SapiSyncManager implements SyncManagerI {
private static final int FULL_SYNC_DOWNLOAD_LIMIT = 300;
private static final String TAG_LOG = "SapiSyncManager";
private static final JSONObject REMOVED_ITEM = new JSONObject();
protected static final String UPLOAD_DATE_FIELD = "date";
protected static final String CRC_FIELD = "date";
protected static final String SIZE_FIELD = "size";
protected static final String ID_FIELD = "id";
protected static final String NAME_FIELD = "name";
private SyncConfig syncConfig = null;
private SapiSyncHandler sapiSyncHandler = null;
private SapiSyncStatus syncStatus = null;
private String deviceId = null;
private JSONArray deletedArray = null;
private HttpDownloader downloader = null;
// Holds the list of twins found during the download phase, those items must
// not be uploaded to the server later in the upload phase
private Hashtable twins = null;
/**
* Unique instance of a BasicSyncListener which is used when the user does
* not set up a listener in the SyncSource. In order to avoid the creation
* of multiple instances of this class we use this static variable
*/
private static SyncListener basicListener = null;
/**
* This is the flag used to indicate that the sync shall be cancelled. Users
* can call the cancel (@see cancel) method to cancel the current sync
*/
private boolean cancel;
private SyncSource currentSource = null;
private Hashtable localUpdates = null;
private Hashtable localDeletes = null;
private Enumeration localUpdatesEnum = null;
private Enumeration localDeletesEnum = null;
private String addedServerUrl = null;
private String updatedServerUrl = null;
private long downloadNextAnchor;
private long clientServerTimeDifference = 0;
private SapiSyncStrategy strategy = null;
private SapiSyncUtils utils = null;
private MappingTable mapping;
// TODO FIXME: fill this hashtable
private Hashtable localRenamed = null;
private boolean loggedIn = false;
/**
* <code>SapiSyncManager</code> constructor
* @param config
*/
public SapiSyncManager(SyncConfig config, DeviceConfigI devConfig) {
this.syncConfig = config;
this.sapiSyncHandler = new SapiSyncHandler(
StringUtil.extractAddressFromUrl(syncConfig.getSyncUrl()),
syncConfig.getUserName(),
syncConfig.getPassword());
strategy = new SapiSyncStrategy(sapiSyncHandler, REMOVED_ITEM);
utils = new SapiSyncUtils();
this.deviceId = devConfig.getDevID();
}
/**
* Force a specific SapiSyncHandler to be used for testing purposes.
* @param sapiSyncHandler
*/
public void setSapiSyncHandler(SapiSyncHandler sapiSyncHandler) {
this.sapiSyncHandler = sapiSyncHandler;
strategy.setSapiSyncHandler(sapiSyncHandler);
}
/**
* Synchronizes the given source, using the preferred sync mode defined for
* that SyncSource.
*
* @param source the SyncSource to synchronize
*
* @throws SyncException If an error occurs during synchronization
*
*/
public void sync(SyncSource source) throws SyncException {
sync(source, source.getSyncMode(), false);
}
public void sync(SyncSource source, boolean askServerDevInf) throws SyncException {
sync(source, source.getSyncMode(), askServerDevInf);
}
public void sync(SyncSource src, int syncMode) {
sync(src, syncMode, false);
}
/**
* Synchronizes the given source, using the given sync mode.
*
* @param source the SyncSource to synchronize
* @param syncMode the sync mode
* @param askServerDevInf true if the engine shall ask for server caps
*
* @throws SyncException If an error occurs during synchronization
*/
public synchronized void sync(SyncSource src, int syncMode, boolean askServerDevInf)
throws SyncException {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Starting sync");
}
cancel = false;
if (basicListener == null) {
basicListener = new BasicSyncListener();
}
currentSource = src;
boolean resume = false;
syncStatus = new SapiSyncStatus(src.getName());
try {
syncStatus.load();
// If the sync was interrupted, then we shall resume
resume = syncStatus.getInterrupted();
} catch (Exception e) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Cannot load sync status, use an empty one");
}
}
// If we are not resuming, then we can reset the status
if (resume) {
// We need to understand where the sync got stopped and if a single item
// shall be resumed
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Resume is active");
}
} else {
try {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Resume is not active");
}
syncStatus.reset();
syncStatus.setInterrupted(true);
} catch (IOException ioe) {
Log.error(TAG_LOG, "Cannot reset status", ioe);
}
}
mapping = new MappingTable(src.getName());
SapiSyncAnchor sapiAnchor = (SapiSyncAnchor)src.getSyncAnchor();
if (sapiAnchor.getDownloadAnchor() == 0 && sapiAnchor.getUploadAnchor() == 0) {
// This is the first sync with this server, clean the mapping
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Resetting mapping");
}
try {
mapping.reset();
} catch (Exception e) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "The mapping store does not exist, use an empty one");
}
}
} else {
try {
mapping.load();
} catch (Exception e) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "The mapping store does not exist, use an empty one");
}
}
}
// Reset the loggedIn flag
loggedIn = false;
// Init twins vector
twins = new Hashtable();
try {
Throwable downloadNonBlockingError = null;
Throwable uploadNonBlockingError = null;
// Set the basic properties in the sync status
syncStatus.setRemoteUri(src.getConfig().getRemoteUri());
syncStatus.setInterrupted(true);
syncStatus.setLocUri(src.getName());
syncStatus.setStartTime(System.currentTimeMillis());
syncStatus.save();
getSyncListenerFromSource(src).startSession();
getSyncListenerFromSource(src).startConnecting();
cancelSyncIfNeeded(src);
performLogin();
performInitializationPhase(src, resume);
cancelSyncIfNeeded(src);
getSyncListenerFromSource(src).syncStarted(syncMode);
try {
// The download anchor is updated once it is received from the server
performDownloadPhase(src, getActualDownloadSyncMode(src), resume);
} catch (NonBlockingSyncException nbse) {
// Carries on
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG,
"Caught a non blocking exception (code " + nbse.getCode() +
") during download, sync will continue");
}
downloadNonBlockingError = nbse;
}
cancelSyncIfNeeded(src);
try {
long newUploadAnchor = (new Date()).getTime();
performUploadPhase(src, getActualUploadSyncMode(src), resume);
// If we had no error so far, then we update the anchor
SapiSyncAnchor anchor = (SapiSyncAnchor)src.getSyncAnchor();
anchor.setUploadAnchor(newUploadAnchor);
// This catch block is not used so far, but it is here in case in the future we introduce
// non-blocking exceptions in the upload phase too.
} catch (NonBlockingSyncException nbse) {
// Carries on
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG,
"Caught a non blocking exception (code " + nbse.getCode() +
") during upload, sync will continue");
}
uploadNonBlockingError = nbse;
}
cancelSyncIfNeeded(src);
getSyncListenerFromSource(src).startFinalizing();
performFinalizationPhase(src);
getSyncListenerFromSource(src).endFinalizing();
if (uploadNonBlockingError != null) { // upload errors prevail
throw uploadNonBlockingError;
} else if (downloadNonBlockingError != null) {
throw downloadNonBlockingError;
} else {
syncStatus.setInterrupted(false);
syncStatus.setStatusCode(SyncListener.SUCCESS);
}
// This catch block can be reached directly when sync-blocking exceptions are thrown, or
// indirectly when non-blocking exceptions are re-thrown at the end of the sync (a few lines
// above).
} catch (Throwable t) {
Log.error(TAG_LOG, "Error while synchronizing", t);
syncStatus.setSyncException(t);
SyncException se;
if (t instanceof SyncException) {
se = (SyncException)t;
} else {
se = new SyncException(SyncException.CLIENT_ERROR, "Generic error");
}
int syncStatusCode = getListenerStatusFromSyncException(se);
syncStatus.setStatusCode(syncStatusCode);
throw se;
} finally {
try {
mapping.save();
} catch (Exception e) {
Log.error(TAG_LOG, "Cannot save mapping store", e);
}
syncStatus.setEndTime(System.currentTimeMillis());
try {
syncStatus.save();
} catch (IOException ioe) {
Log.error(TAG_LOG, "Cannot save sync status", ioe);
}
// We must guarantee this method is invoked in all cases
getSyncListenerFromSource(src).endSession(syncStatus);
currentSource = null;
}
}
public void cancel() {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Cancelling sync");
}
cancel = true;
if(sapiSyncHandler != null) {
sapiSyncHandler.cancel();
}
if(downloader != null) {
downloader.cancel();
}
if(currentSource != null) {
currentSource.cancel();
}
}
private void cancelSyncIfNeeded(SyncSource src) throws SyncException {
if(cancel) {
performFinalizationPhase(null);
throw new SyncException(SyncException.CANCELLED, "Sync got cancelled");
}
}
private void performInitializationPhase(SyncSource src, boolean resume)
throws SyncException, JSONException
{
// Prepare the source for the sync
src.beginSync(getActualUploadSyncMode(src), resume);
int downloadSyncMode = getActualDownloadSyncMode(src);
int uploadSyncMode = getActualUploadSyncMode(src);
boolean incrementalDownload = isIncrementalSync(downloadSyncMode);
boolean incrementalUpload = isIncrementalSync(uploadSyncMode);
// In case of full download we can ignore local changes because during a
// full download we only need to detect changes
if (incrementalDownload) {
strategy.prepareUpload(src, downloadSyncMode, uploadSyncMode,
resume, mapping, incrementalDownload,
incrementalUpload, twins);
}
localUpdates = strategy.getLocalUpdates();
if (localUpdates != null) {
localUpdatesEnum = localUpdates.elements();
}
localDeletes = strategy.getLocalDeletes();
if (localDeletes != null) {
Vector itemStatuses = new Vector();
localDeletesEnum = localDeletes.elements();
// If the client reported deletes, then we can update the mapping
// accordinlgly
while(localDeletesEnum.hasMoreElements()) {
SyncItem item = (SyncItem)localDeletesEnum.nextElement();
String guid = mapping.getGuid(item.getKey());
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Removing entry from mapping " + guid);
}
mapping.remove(guid);
// Also notify the source as this command was propagated with
// success so it won't provide it again
ItemStatus status = new SapiItemStatus(item, SyncSource.SUCCESS_STATUS);
itemStatuses.addElement(status);
}
src.applyItemsStatus(itemStatuses);
}
}
private void performUploadPhase(SyncSource src, int syncMode, boolean resume) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Starting upload phase with mode: " + syncMode);
}
Vector sourceStatus = new Vector();
boolean incremental = isIncrementalSync(syncMode);
String remoteUri = src.getConfig().getRemoteUri();
int totalSending = -1;
if (incremental) {
totalSending = src.getClientAddNumber() + src.getClientReplaceNumber();
} else {
totalSending = src.getClientItemsNumber();
}
int maxSending = -1;
// Apply upload filter to the total items count
SyncFilter syncFilter = src.getFilter();
if(syncFilter != null) {
Filter uploadFilter = incremental ?
syncFilter.getIncrementalUploadFilter() :
syncFilter.getFullUploadFilter();
if(uploadFilter != null && uploadFilter.isEnabled() &&
uploadFilter.getType() == Filter.ITEMS_COUNT_TYPE)
{
maxSending = uploadFilter.getCount();
// If we are resuming a sync, then we must consider the items
// that were sent in previous sync
if (resume) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Since we are resuming count the items previously sent "
+ syncStatus.getSentAddNumber());
}
maxSending = maxSending - syncStatus.getSentAddNumber();
}
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Setting up items count filter with maxSending=" + maxSending);
}
}
}
// Exclude twins from total items count
totalSending -= twins.size();
if(totalSending > 0) {
if(maxSending > 0 && totalSending > maxSending) {
totalSending = maxSending;
}
getSyncListenerFromSource(src).startSending(totalSending, 0, 0);
}
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Uploading items count: " + totalSending);
}
int uploadedCount = 0;
JSONSyncItem item = getNextItemToUpload(src, incremental);
try {
while(item != null && itemsCountFilter(maxSending, uploadedCount)) {
try {
// Exclude twins
if(twins.get(item.getKey()) != null) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Exclude twin item to be uploaded: "
+ item.getKey());
}
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.SUCCESS_STATUS));
continue;
}
// Notify the listener
if (item.getState() == SyncItem.STATE_NEW) {
getSyncListenerFromSource(src).itemAddSendingStarted(item.getKey(), null, item.getContentSize());
} else if (item.getState() == SyncItem.STATE_UPDATED) {
getSyncListenerFromSource(src).itemReplaceSendingStarted(item.getKey(), null, item.getContentSize());
}
try {
String remoteKey = null;
// Sets the status as interrupted so that if the
// client crashes badly we still remember this fact
if (item.getState() == SyncItem.STATE_UPDATED) {
String luid = item.getKey();
// Check if the item key has been updated
if(item.isItemKeyUpdated()) {
luid = item.getOldKey();
}
remoteKey = mapping.getGuid(luid);
item.setGuid(remoteKey);
}
// Check if this is only a file rename.
// Using save-metadata to update the file name doesn't
// work becouse it requires the file content to be
// re-uploaded
String newCrc;
if(item.isItemKeyUpdated() && !item.isItemContentUpdated()) {
// This is only a item rename
newCrc = sapiSyncHandler.updateItemName(remoteUri, remoteKey,
item.getJSONFileObject().getName());
} else {
remoteKey = sapiSyncHandler.prepareItemUpload(item, remoteUri);
item.setGuid(remoteKey);
// TODO FIXME
newCrc = "0";
}
// Update the mapping table
if(item.getState() == SyncItem.STATE_UPDATED) {
mapping.update(remoteKey, item.getKey(),
newCrc, item.getContentName());
} else {
mapping.add(remoteKey, item.getKey(),
newCrc, item.getContentName());
}
} catch (SapiException e) {
verifyErrorInUploadResponse(e, item, item.getGuid(), sourceStatus);
}
syncStatus.setSentItemStatus(item.getGuid(), item.getKey(),
item.getState(), SyncSource.SUCCESS_STATUS);
try {
syncStatus.save();
} catch (Exception e) {
Log.error(TAG_LOG, "Cannot save sync status", e);
}
// Set the item status
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.SUCCESS_STATUS));
} catch(NonBlockingSyncException nbse) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "The error uploading item is non blocking, continue to upload");
}
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.ERROR_STATUS));
} catch(SyncException ex) {
// We must distinguish between exceptions that interrupt the
// sync and exceptions that do not interrupt it
// TODO FIXME
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.ERROR_STATUS));
//relaunch managed sync exception
throw ex;
} catch(Exception ex) {
//generic errors catch, just in case...
if(Log.isLoggable(Log.ERROR)) {
Log.error(TAG_LOG, "Failed to upload item with key: " +
item.getKey(), ex);
}
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.ERROR_STATUS));
// TODO FIXME: what shall we do in this case?
} finally {
// Notify the listener
if (item.getState() == SyncItem.STATE_NEW) {
getSyncListenerFromSource(src).itemAddSendingEnded(item.getKey(), null);
} else if (item.getState() == SyncItem.STATE_UPDATED) {
getSyncListenerFromSource(src).itemReplaceSendingEnded(item.getKey(), null);
}
uploadedCount++;
item = getNextItemToUpload(src, incremental);
cancelSyncIfNeeded(src);
}
}
} finally {
src.applyItemsStatus(sourceStatus);
}
}
private boolean itemsCountFilter(int max, int current) {
if(max >= 0) {
return current < max;
} else {
return true;
}
}
private JSONSyncItem getNextItemToUpload(SyncSource src, boolean incremental) {
if(incremental) {
JSONSyncItem item = (JSONSyncItem)src.getNextNewItem();
if (item == null) {
// New items are over, now check for updates
if (localUpdatesEnum != null && localUpdatesEnum.hasMoreElements()) {
item = (JSONSyncItem)localUpdatesEnum.nextElement();
}
}
return item;
} else {
return (JSONSyncItem)src.getNextItem();
}
}
private void performIncrementalDownload(SyncSource src, boolean resume) throws JSONException {
String remoteUri = src.getConfig().getRemoteUri();
SapiSyncAnchor sapiAnchor = (SapiSyncAnchor)src.getConfig().getSyncAnchor();
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Last download anchor is: " + sapiAnchor.getDownloadAnchor());
}
Date anchor = new Date(sapiAnchor.getDownloadAnchor());
SapiSyncHandler.ChangesSet changesSet = null;
try {
changesSet = sapiSyncHandler.getIncrementalChanges(anchor, remoteUri);
} catch (SapiException e) {
String errorMessage = "Client error while getting incremental changes";
utils.processCommonSapiExceptions(e, errorMessage, false);
utils.processCustomSapiExceptions(e, errorMessage, true);
}
if (changesSet != null) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "There are changes pending on the server");
}
// Use the above value as timestamp for the next sync
downloadNextAnchor = changesSet.timeStamp;
SapiSyncHandler.FullSet addedInfo = null;
SapiSyncHandler.FullSet updatedInfo = null;
SapiSyncHandler.FullSet deletedInfo = null;
addedInfo = fetchItemsInfo(src, changesSet.added);
updatedInfo = fetchItemsInfo(src, changesSet.updated);
deletedArray = changesSet.deleted;
if (addedInfo != null) {
applyFullSet(src, addedInfo, addedInfo.serverUrl, mapping, twins, false);
}
if (updatedInfo != null) {
applyFullSet(src, updatedInfo, updatedInfo.serverUrl, mapping, twins, false);
}
if (deletedArray != null) {
applyDelItems(src, deletedArray);
}
}
}
private SapiSyncHandler.FullSet fetchItemsInfo(SyncSource src, JSONArray items)
throws JSONException
{
SapiSyncHandler.FullSet fullSet = null;
if (items != null) {
String dataTag = utils.getDataTag(src);
JSONArray itemsId = new JSONArray();
for(int i=0;i<items.length();++i) {
int id = Integer.parseInt(items.getString(i));
itemsId.put(id);
}
if (itemsId.length() > 0) {
// Ask for these items
fullSet = sapiSyncHandler.getItems(src.getConfig().getRemoteUri(), dataTag,
itemsId, null, null, null);
if (fullSet != null && fullSet.items != null) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "items = " + fullSet.items.toString());
}
}
}
}
return fullSet;
}
private void performLogin() {
if (loggedIn) {
return;
}
try {
// Perform a login to avoid multiple authentications
sapiSyncHandler.login(deviceId);
clientServerTimeDifference = sapiSyncHandler.getDeltaTime();
strategy.setClientServerTimeDifference(clientServerTimeDifference);
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Difference in time between server and client is " + clientServerTimeDifference);
}
loggedIn = true;
} catch (SapiException e) {
Log.error(TAG_LOG, "Cannot perform login call, this is a non blocking error", e);
}
}
private void performFullDownload(SyncSource src, boolean resume) throws JSONException {
String remoteUri = src.getConfig().getRemoteUri();
SyncFilter syncFilter = src.getFilter();
int downloadLimit = FULL_SYNC_DOWNLOAD_LIMIT;
String dataTag = utils.getDataTag(src);
int offset = 0;
boolean done = false;
downloadNextAnchor = -1;
String serverUrl = null;
int i = 0;
do {
// We need to get all items on the server to be able to do effective
// twin detection.
SapiSyncHandler.FullSet fullSet = sapiSyncHandler.getItems(remoteUri, dataTag, null,
Integer.toString(downloadLimit),
Integer.toString(offset), null);
if (downloadNextAnchor == -1) {
downloadNextAnchor = fullSet.timeStamp;
serverUrl = fullSet.serverUrl;
}
if (fullSet != null && fullSet.items != null && fullSet.items.length() > 0) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "items = " + fullSet.items.toString());
}
applyFullSet(src, fullSet, serverUrl, mapping, twins, true);
offset += fullSet.items.length();
if ((fullSet.items.length() < FULL_SYNC_DOWNLOAD_LIMIT)) {
done = true;
}
} else {
done = true;
}
i++;
} while(!done);
}
private void applyFullSet(SyncSource src, SapiSyncHandler.FullSet fullSet, String serverUrl,
MappingTable mapping, Hashtable twins, boolean deepTwinSearch)
throws JSONException
{
for(int i=0;i<fullSet.items.length();++i) {
JSONObject item = fullSet.items.getJSONObject(i);
// Check if this item is a twin or in conflict with local
// changes
String guid = item.getString(ID_FIELD);
String luid = mapping.getLuid(guid);
char state;
if (luid != null) {
state = SyncItem.STATE_UPDATED;
} else {
state = SyncItem.STATE_NEW;
}
// TODO FIXME: need the local updates/deletes to work
boolean download = strategy.resolveTwinAndConflicts(src, item, null, null,
serverUrl, mapping, twins, deepTwinSearch);
if (download) {
// The item needs to be downloaded
// TODO FIXME: use the proper item state
applyNewUpdToSyncSource(src, item, state, serverUrl);
}
}
}
private void performDownloadPhase(SyncSource src, int syncMode, boolean resume)
throws SyncException {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Starting download phase with mode: " + syncMode);
}
if (syncMode == SyncSource.FULL_DOWNLOAD) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Performing full download");
}
try {
performFullDownload(src, resume);
updateDownloadAnchor((SapiSyncAnchor) src.getConfig().getSyncAnchor(), downloadNextAnchor);
} catch (JSONException je) {
Log.error(TAG_LOG, "Cannot parse server data", je);
throw new SyncException(SyncException.CLIENT_ERROR, je.toString());
}
} else if (syncMode == SyncSource.INCREMENTAL_DOWNLOAD) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Performing incremental download");
}
try {
performIncrementalDownload(src, resume);
updateDownloadAnchor((SapiSyncAnchor) src.getConfig().getSyncAnchor(), downloadNextAnchor);
} catch (JSONException je) {
Log.error(TAG_LOG, "Cannot parse server data", je);
throw new SyncException(SyncException.CLIENT_ERROR, je.toString());
}
}
}
private int countActualItems(JSONArray items) throws JSONException{
int count = 0;
if (items != null) {
for(int i=0;i<items.length();++i) {
JSONObject item = items.getJSONObject(i);
if (item != REMOVED_ITEM) {
count++;
}
}
}
return count;
}
private int countActualDeletedItems(JSONArray items) throws JSONException {
if (items != null) {
int count = 0;
for(int i=0;i<items.length();++i) {
String guid = items.getString(i);
if (guid != null && guid.length() > 0) {
count++;
}
}
return count;
} else {
return 0;
}
}
/**
* Apply the given items to the source, returning whether the source can
* accept further items.
*
* @param src
* @param items
* @param state
* @param deepTwinSearch
* @return
* @throws SyncException
* @throws JSONException
*/
private boolean applyNewUpdToSyncSource(SyncSource src, JSONArray items,
char state, String serverUrl,
boolean resume)
throws SyncException, JSONException
{
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "apply new update items to source " + src.getName());
}
boolean done = false;
// Apply these changes into the sync source
for(int k=0; k<items.length() && !done; ++k) {
cancelSyncIfNeeded(src);
JSONObject item = items.getJSONObject(k);
// We must skip items that were removed by the initialization phase
if (item == REMOVED_ITEM) {
continue;
}
String guid = item.getString(ID_FIELD);
long size = Long.parseLong(item.getString(SIZE_FIELD));
String luid;
// Get the lastupdated property used as item crc
String crc = "" + item.getLong(CRC_FIELD);
if (state == SyncItem.STATE_UPDATED) {
luid = mapping.getLuid(guid);
} else {
// This is an add. If the item is already present in the mapping
// then this is an add
luid = guid;
}
// If the client doesn't have the luid we change the state to new
if(StringUtil.isNullOrEmpty(luid)) {
state = SyncItem.STATE_NEW;
}
// Create the item
JSONSyncItem syncItem = (JSONSyncItem)utils.createSyncItem(src, luid, state, size, item, serverUrl);
syncItem.setGuid(guid);
// Notify the listener
if (syncItem.getState() == SyncItem.STATE_NEW) {
getSyncListenerFromSource(src).itemAddReceivingStarted(syncItem.getKey(), syncItem.getParent(), size);
} else if (syncItem.getState() == SyncItem.STATE_UPDATED) {
getSyncListenerFromSource(src).itemReplaceReceivingStarted(syncItem.getKey(), syncItem.getParent(), size);
}
// Was the item renamed
if (item.has("oldkey")) {
syncItem.setOldKey(item.getString("oldkey"));
syncItem.setItemKeyUpdated(true);
} else {
syncItem.setItemKeyUpdated(false);
}
// Apply the item to the source
Vector tmpItems = new Vector();
tmpItems.addElement(syncItem);
src.applyChanges(tmpItems);
if (syncItem.getSyncStatus() != -1 && syncItem.getGuid() != null && syncItem.getKey() != null) {
syncStatus.setReceivedItemStatus(syncItem.getGuid(), syncItem.getKey(),
syncItem.getState(), syncItem.getSyncStatus());
// and the mapping table (if luid and guid are different)
if (state == SyncItem.STATE_NEW && !syncItem.getGuid().equals(syncItem.getKey())) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Updating mapping info for: " +
syncItem.getGuid() + "," + syncItem.getKey());
}
mapping.add(syncItem.getGuid(), syncItem.getKey(), crc,
syncItem.getContentName());
} else if (state == SyncItem.STATE_UPDATED && item.has("oldkey")) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Updating mapping info for renamed item: " +
syncItem.getGuid() + "," + syncItem.getKey());
}
mapping.update(syncItem.getGuid(), syncItem.getKey(), crc, syncItem.getContentName());
}
}
// Notify the listener
if (syncItem.getState() == SyncItem.STATE_NEW) {
getSyncListenerFromSource(src).itemAddReceivingEnded(syncItem.getKey(), syncItem.getParent());
} else if (syncItem.getState() == SyncItem.STATE_UPDATED) {
getSyncListenerFromSource(src).itemReplaceReceivingEnded(syncItem.getKey(), syncItem.getParent());
}
}
return done;
}
private boolean applyNewUpdToSyncSource(SyncSource src, JSONObject item,
char state, String serverUrl)
throws SyncException, JSONException
{
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "apply new update items to source " + src.getName());
}
boolean done = false;
cancelSyncIfNeeded(src);
// We must skip items that were removed by the initialization phase
if (item == REMOVED_ITEM) {
return false;
}
String guid = item.getString(ID_FIELD);
long size = Long.parseLong(item.getString(SIZE_FIELD));
String luid;
// Get the lastupdated property used as item crc
String crc = "" + item.getLong(CRC_FIELD);
if (state == SyncItem.STATE_UPDATED) {
luid = mapping.getLuid(guid);
} else {
// This is an add. If the item is already present in the mapping
// then this is an add
luid = guid;
}
// If the client doesn't have the luid we change the state to new
if(StringUtil.isNullOrEmpty(luid)) {
state = SyncItem.STATE_NEW;
}
// Create the item
JSONSyncItem syncItem = (JSONSyncItem)utils.createSyncItem(src, luid, state, size, item, serverUrl);
syncItem.setGuid(guid);
// Notify the listener
if (syncItem.getState() == SyncItem.STATE_NEW) {
getSyncListenerFromSource(src).itemAddReceivingStarted(syncItem.getKey(), syncItem.getParent(), size);
} else if (syncItem.getState() == SyncItem.STATE_UPDATED) {
getSyncListenerFromSource(src).itemReplaceReceivingStarted(syncItem.getKey(), syncItem.getParent(), size);
}
// Was the item renamed
if (item.has("oldkey")) {
syncItem.setOldKey(item.getString("oldkey"));
syncItem.setItemKeyUpdated(true);
} else {
syncItem.setItemKeyUpdated(false);
}
// Apply the item to the source
Vector tmpItems = new Vector();
tmpItems.addElement(syncItem);
src.applyChanges(tmpItems);
if (syncItem.getSyncStatus() != -1 && syncItem.getGuid() != null && syncItem.getKey() != null) {
syncStatus.setReceivedItemStatus(syncItem.getGuid(), syncItem.getKey(),
syncItem.getState(), syncItem.getSyncStatus());
// and the mapping table (if luid and guid are different)
if (state == SyncItem.STATE_NEW && !syncItem.getGuid().equals(syncItem.getKey())) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Updating mapping info for: " +
syncItem.getGuid() + "," + syncItem.getKey());
}
mapping.add(syncItem.getGuid(), syncItem.getKey(), crc,
syncItem.getContentName());
} else if (state == SyncItem.STATE_UPDATED && item.has("oldkey")) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "Updating mapping info for renamed item: " +
syncItem.getGuid() + "," + syncItem.getKey());
}
mapping.update(syncItem.getGuid(), syncItem.getKey(), crc, syncItem.getContentName());
}
}
// Notify the listener
if (syncItem.getState() == SyncItem.STATE_NEW) {
getSyncListenerFromSource(src).itemAddReceivingEnded(syncItem.getKey(), syncItem.getParent());
} else if (syncItem.getState() == SyncItem.STATE_UPDATED) {
getSyncListenerFromSource(src).itemReplaceReceivingEnded(syncItem.getKey(), syncItem.getParent());
}
return done;
}
private void applyDelItems(SyncSource src, JSONArray removed) throws SyncException, JSONException
{
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "applyDelItems");
}
Vector delItems = new Vector();
for(int i=0;i < removed.length();++i) {
String guid = removed.getString(i);
if (guid != null && guid.length() > 0) {
String luid = mapping.getLuid(guid);
if (luid == null) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Cannot delete item with unknown luid " + guid);
}
} else {
SyncItem delItem = new SyncItem(luid, src.getType(),
SyncItem.STATE_DELETED, null);
delItem.setGuid(guid);
delItems.addElement(delItem);
getSyncListenerFromSource(src).itemDeleted(delItem);
syncStatus.addReceivedItem(guid, luid, delItem.getState(), SyncSource.SUCCESS_STATUS);
}
}
}
if (delItems.size() > 0) {
src.applyChanges(delItems);
}
}
private void performFinalizationPhase(SyncSource src) throws SyncException {
try {
sapiSyncHandler.logout();
} catch (SapiException e) {
String errorMessage = "Cannot logout";
utils.processCommonSapiExceptions(e, errorMessage, false);
utils.processCustomSapiExceptions(e, errorMessage, true);
}
if(src != null) {
src.endSync();
}
}
private SyncListener getSyncListenerFromSource(SyncSource source) {
SyncListener slistener = source.getListener();
if(slistener != null) {
return slistener;
} else {
return basicListener;
}
}
private int getActualSyncMode(SyncSource src, int syncMode) {
SyncAnchor anchor = src.getSyncAnchor();
if(anchor instanceof SapiSyncAnchor) {
SapiSyncAnchor sapiAnchor = (SapiSyncAnchor)anchor;
if(syncMode == SyncSource.INCREMENTAL_SYNC) {
if(sapiAnchor.getUploadAnchor() == 0) {
return SyncSource.FULL_SYNC;
}
} else if(syncMode == SyncSource.INCREMENTAL_UPLOAD) {
if(sapiAnchor.getUploadAnchor() == 0) {
return SyncSource.FULL_UPLOAD;
}
} else if(syncMode == SyncSource.INCREMENTAL_DOWNLOAD) {
if(sapiAnchor.getDownloadAnchor() == 0) {
return SyncSource.FULL_DOWNLOAD;
}
}
return syncMode;
} else {
throw new SyncException(SyncException.ILLEGAL_ARGUMENT,
"Invalid source anchor format");
}
}
private int getActualDownloadSyncMode(SyncSource src) {
SyncAnchor anchor = src.getSyncAnchor();
if(anchor instanceof SapiSyncAnchor) {
SapiSyncAnchor sapiAnchor = (SapiSyncAnchor)anchor;
if(sapiAnchor.getDownloadAnchor() > 0) {
return SyncSource.INCREMENTAL_DOWNLOAD;
} else {
return SyncSource.FULL_DOWNLOAD;
}
} else {
throw new SyncException(SyncException.ILLEGAL_ARGUMENT,
"Invalid source anchor format");
}
}
private int getActualUploadSyncMode(SyncSource src) {
SyncAnchor anchor = src.getSyncAnchor();
if(anchor instanceof SapiSyncAnchor) {
SapiSyncAnchor sapiAnchor = (SapiSyncAnchor)anchor;
if(sapiAnchor.getUploadAnchor() > 0) {
return SyncSource.INCREMENTAL_UPLOAD;
} else {
return SyncSource.FULL_UPLOAD;
}
} else {
throw new SyncException(SyncException.ILLEGAL_ARGUMENT,
"Invalid source anchor format");
}
}
private boolean isIncrementalSync(int syncMode) {
return (syncMode == SyncSource.INCREMENTAL_SYNC) ||
(syncMode == SyncSource.INCREMENTAL_DOWNLOAD) ||
(syncMode == SyncSource.INCREMENTAL_UPLOAD);
}
private int getListenerStatusFromSyncException(SyncException se) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "getting listener status for " + se.getCode());
}
int statusCode;
switch (se.getCode()) {
case SyncException.AUTH_ERROR:
statusCode = SyncListener.INVALID_CREDENTIALS;
break;
case SyncException.FORBIDDEN_ERROR:
statusCode = SyncListener.FORBIDDEN_ERROR;
break;
case SyncException.CONN_NOT_FOUND:
statusCode = SyncListener.CONN_NOT_FOUND;
break;
case SyncException.READ_SERVER_RESPONSE_ERROR:
statusCode = SyncListener.READ_SERVER_RESPONSE_ERROR;
break;
case SyncException.WRITE_SERVER_REQUEST_ERROR:
statusCode = SyncListener.WRITE_SERVER_REQUEST_ERROR;
break;
case SyncException.SERVER_CONNECTION_REQUEST_ERROR:
statusCode = SyncListener.SERVER_CONNECTION_REQUEST_ERROR;
break;
case SyncException.BACKEND_AUTH_ERROR:
statusCode = SyncListener.BACKEND_AUTH_ERROR;
break;
case SyncException.NOT_FOUND_URI_ERROR:
statusCode = SyncListener.URI_NOT_FOUND_ERROR;
break;
case SyncException.CONNECTION_BLOCKED_BY_USER:
statusCode = SyncListener.CONNECTION_BLOCKED_BY_USER;
break;
case SyncException.SMART_SLOW_SYNC_UNSUPPORTED:
statusCode = SyncListener.SMART_SLOW_SYNC_UNSUPPORTED;
break;
case SyncException.CLIENT_ERROR:
statusCode = SyncListener.CLIENT_ERROR;
break;
case SyncException.ACCESS_ERROR:
statusCode = SyncListener.ACCESS_ERROR;
break;
case SyncException.DATA_NULL:
statusCode = SyncListener.DATA_NULL;
break;
case SyncException.ILLEGAL_ARGUMENT:
statusCode = SyncListener.ILLEGAL_ARGUMENT;
break;
case SyncException.SERVER_ERROR:
statusCode = SyncListener.SERVER_ERROR;
break;
case SyncException.SERVER_BUSY:
statusCode = SyncListener.SERVER_BUSY;
break;
case SyncException.BACKEND_ERROR:
statusCode = SyncListener.BACKEND_ERROR;
break;
case SyncException.CANCELLED:
statusCode = SyncListener.CANCELLED;
break;
case SyncException.NOT_SUPPORTED:
statusCode = SyncListener.NOT_SUPPORTED;
break;
case SyncException.SD_CARD_UNAVAILABLE:
statusCode = SyncListener.SD_CARD_UNAVAILABLE;
break;
case SyncException.ERR_READING_COMPRESSED_DATA:
statusCode = SyncListener.COMPRESSED_RESPONSE_ERROR;
break;
case SyncException.DEVICE_FULL:
statusCode = SyncListener.SERVER_FULL_ERROR;
break;
case SyncException.LOCAL_DEVICE_FULL:
statusCode = SyncListener.LOCAL_CLIENT_FULL_ERROR;
break;
default:
statusCode = SyncListener.GENERIC_ERROR;
break;
}
return statusCode;
}
private void updateDownloadAnchor(SapiSyncAnchor sapiAnchor, long newDownloadAnchor) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Updating download anchor to " + newDownloadAnchor);
}
sapiAnchor.setDownloadAnchor(newDownloadAnchor);
}
/**
* Common code used to verify specific error in upload sapi
* (size mismatch, over quota etc)
*
* @param sapiException
* @param item
* @param remoteKey
* @param sourceStatus
* @throws SyncException
*/
private void verifyErrorInUploadResponse(
SapiException sapiException,
SyncItem item,
String remoteKey,
Vector sourceStatus)
throws SyncException
{
//manage common errors
utils.processCommonSapiExceptions(sapiException, "Cannot upload item", false);
//manage custom errors
utils.processCustomSapiExceptions(sapiException, "Cannot upload item", false);
if (SapiException.MED_1002.equals(sapiException.getCode())
|| SapiException.CUS_0001.equals(sapiException.getCode())) {
// An item could not be fully uploaded
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Error uploading item " + item.getKey());
}
// The size of the uploading media does not match the one declared
item.setGuid(remoteKey);
syncStatus.addSentItem(item.getGuid(), item.getKey(),
item.getState(), SyncSource.INTERRUPTED_STATUS);
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.INTERRUPTED_STATUS));
// Interrupt the sync with a network error
throw new SyncException(SyncException.CONN_NOT_FOUND, sapiException.getMessage());
} else if (SapiException.MED_1007.equals(sapiException.getCode())) {
// An item could not be uploaded because user quota on
// server exceeded
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Server quota overflow error");
}
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.SERVER_FULL_ERROR_STATUS));
throw new SyncException(SyncException.DEVICE_FULL, "Server quota exceeded");
} else if (SapiException.MED_1000.equals(sapiException.getCode())) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Unsupported media error (MEDIA-1000)");
}
// If we set an error status, the item will be re-sent at the next
// sync.... TODO FIXME
sourceStatus.addElement(new SapiItemStatus(item, SyncSource.ERROR_STATUS));
// This is a non blocking exception
throw new NonBlockingSyncException(SyncException.SERVER_ERROR, "Item not supported by server");
} else {
throw new SyncException(
SyncException.SERVER_ERROR,
"Cannot upload item, error in SAPI response: " + sapiException.getMessage());
}
}
/**
* Translates the HttpDownloader.DownloadListener calls into SyncListener calls.
*/
private class DownloadSyncListener implements HttpDownloader.DownloadListener {
private SyncListener syncListener = null;
private String itemKey = null;
private String itemParent = null;
private char itemState;
public DownloadSyncListener(SyncItem item, SyncListener syncListener) {
this.syncListener = syncListener;
this.itemKey = item.getKey();
this.itemParent = item.getParent();
this.itemState = item.getState();
}
public void downloadStarted(long totalSize) {
}
public void downloadProgress(long size) {
if(syncListener != null) {
if(itemState == SyncItem.STATE_NEW) {
syncListener.itemAddReceivingProgress(itemKey, itemParent, size);
} else {
syncListener.itemReplaceReceivingProgress(itemKey, itemParent, size);
}
}
}
public void downloadEnded() {
}
}
}