/*
* 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.sync.client;
import java.util.Vector;
import com.funambol.sync.SourceConfig;
import com.funambol.sync.SyncSource;
import com.funambol.sync.SyncListener;
import com.funambol.sync.SyncException;
import com.funambol.sync.SyncFilter;
import com.funambol.sync.SyncAnchor;
import com.funambol.sync.SyncItem;
import com.funambol.sync.ItemStatus;
import com.funambol.util.Log;
/**
* An abstract implementation of the <i>SyncSource</i> interface, providing
* the basic framework each SyncSource has to implement.
* A developer can choose to extends BaseSyncSource or to implements
* SyncSource directly if needed.
*
* The class BaseSyncSource uses the SyncConfig to store the source
* configuration data. With this class is possible to alter the source
* configuration, which is not permitted by the SyncSource interface.
*/
public abstract class BaseSyncSource implements SyncSource {
private static final String TAG_LOG = "BaseSyncSource";
//--------------------------------------------------------------- Attributes
/** SyncSource configuration */
protected SourceConfig config;
/** Synchronization filter */
protected SyncFilter filter;
/** SyncMode, set by beginSync */
protected int syncMode;
// Item lists
protected SyncItem[] allItems, newItems, updItems, delItems;
// Lists counters
protected int allIndex, newIndex, updIndex, delIndex;
/** The number of items to be sent to the server in the session */
private int clientItemsNumber;
/** The number of items that the server announced to send in the session */
private int serverItemsNumber;
/** The number of new items to be sent to the server in the session */
private int clientAddItemsNumber;
/** The number of replaced items to be sent to the server in the session */
private int clientReplaceItemsNumber;
/** The number of deleted items to be sent to the server in the session */
private int clientDeleteItemsNumber;
/** Status of the sync source summarized in an integer value. See constants
* defined in SyncSource */
protected int globalStatus;
/** Listener of the sync process */
private SyncListener listener;
//------------------------------------------------------------- Constructors
/**
* BaseSyncSource constructor: initialize source config
*/
public BaseSyncSource(SourceConfig config) {
this.config = config;
syncMode = 0;
// Init lists (empty)
allItems = null;
newItems = null;
updItems = null;
delItems = null;
// Init counters
allIndex = newIndex = updIndex = delIndex = 0;
// Init number of chages counters
clientItemsNumber = serverItemsNumber = -1;
clientAddItemsNumber = -1;
clientReplaceItemsNumber = -1;
clientDeleteItemsNumber = -1;
filter = null;
}
//----------------------------------------------------------- Public Methods
/**
* Returns the config of the source. The client can use this method
* to obtain the config object and change some parameter. A setConfig()
* must be called to actually change the source configuration.
*
* @return the config of the source
*/
public SourceConfig getConfig() {
return config;
}
/**
* Sets the config of the source. The client can use this method
* to change the config of the source configuration.
* This operation should not be done while the sync is in progress.
*
*/
public void setConfig(SourceConfig config) {
this.config = config;
}
//------------------------------------------------ SyncSource implementation
/**
* Returns the name of the source
*
* @return the name of the source
*/
public String getName() {
return config.getName();
}
/**
* Returns the source URI
*
* @return the absolute URI of the source
*/
public String getSourceUri() {
return config.getRemoteUri();
}
/**
* Returns the type of the source.
* The types are defined as mime-types, for instance * text/x-vcard).
* @return the type of the source
*/
public String getType() {
return config.getType();
}
/**
* Returns the encoding of the source.
* The encoding can be 'b64' or 'none' only. The standard defines
* also 'des' and '3des' but they are not implemented in this version
* of the APIs.
*
* @return the encoding of the source
*/
public String getEncoding() {
return config.getEncoding();
}
/**
* Returns the preferred sync mode of the source.
* The preferred sync mode is the one that the SyncManager sends
* to the server in the initialization phase. The server can respond
* with a different alert code, to force, for instance, a slow.
*
* @return the preferred sync mode for this source
*/
public int getSyncMode() {
return config.getSyncMode();
}
/**
* Returns the current filter for this SyncSource.
*/
public SyncFilter getFilter() {
return filter;
}
/**
* Set a new filter for this SyncSource
*/
public void setFilter(SyncFilter filter) {
this.filter = filter;
}
/**
* Add a new SyncItem to this source backend.
* The item key after a successful add must contain the local UID,
* that is used by the engine to send the mappings to the server.
* The source must then change the item key accordingly before return.
*
* @param item the SyncItem to add, with the GUID sent by the server.
* The source is resposible to set it to the LUID before
* returning a successful status code.
*
* @return the status code of the operation. It will be returned to
* the server in the response for this item.
*
* @throws SyncException if an unrecoverable error occur, to stop the sync
*/
public abstract int addItem(SyncItem item) throws SyncException ;
/**
* Update a given SyncItem stored in the source backend.
*
* @param item the SyncItem to update. The key of the item is already
* the LUID.
*
* @return the status code of the operation. It will be returned to
* the server in the response for this item.
*
* @throws SyncException if an unrecoverable error occur, to stop the sync
*/
public abstract int updateItem(SyncItem item) throws SyncException ;
/**
* Delete a SyncItem stored in the source backend.
*
* @param key The key of the item to delete.
*
* @return the status code of the operation. It will be returned to
* the server in the response for this item.
*
* @throws SyncException if an unrecoverable error occur, to stop the sync
*/
public abstract int deleteItem(String key) throws SyncException ;
public void applyChanges(Vector syncItems) throws SyncException {
for(int i=0;i<syncItems.size();++i) {
SyncItem item = (SyncItem)syncItems.elementAt(i);
int status;
try {
if (item.getState() == SyncItem.STATE_NEW) {
status = addItem(item);
} else if (item.getState() == SyncItem.STATE_UPDATED) {
status = updateItem(item);
} else {
status = deleteItem(item.getKey());
}
} catch (Exception e) {
status = ERROR_STATUS;
}
item.setSyncStatus(status);
}
}
public void applyItemsStatus(Vector itemsStatus) {
for(int i=0;i<itemsStatus.size();++i) {
ItemStatus status = (ItemStatus)itemsStatus.elementAt(i);
setItemStatus(status.getKey(), status.getStatus());
}
}
/**
* Returns the next item of the store.
**/
public SyncItem getNextItem() throws SyncException {
if (allItems == null) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no items to send for slow sync");
}
return null;
}
if (allIndex<allItems.length) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": sending item "
+allItems[allIndex].getKey());
}
SyncItem ret = getItemContent(allItems[allIndex]);
allIndex++;
return ret;
}
else {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no more items to send for slow sync");
}
// All Items sent, we can free memory
allItems = null;
allIndex = 0;
return null;
}
}
/**
* Returns the next new item of the store
* (not yet sent to the server)
*/
public SyncItem getNextNewItem() throws SyncException {
if (newItems == null) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no new items to send");
}
return null;
}
if (newIndex<newItems.length) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": sending item "
+newItems[newIndex].getKey());
}
SyncItem ret = getItemContent(newItems[newIndex]);
// Move to the next item only if this one has been sent completely.
newIndex++;
return ret;
}
else {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no more new items to send");
}
// All Items sent, we can free memory
newItems = null;
newIndex = 0;
return null;
}
}
/**
* Returns the first/next updated item of the store
* (changed from the last sync)
*/
public SyncItem getNextUpdatedItem() throws SyncException {
if (updItems == null) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no updated items to send");
}
return null;
}
if (updIndex<updItems.length) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": sending item "
+updItems[updIndex].getKey());
}
SyncItem ret = getItemContent(updItems[updIndex]);
// Move to the next item only if this one has been sent completely.
updIndex++;
return ret;
}
else {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no more updated items to send");
}
// All Items sent, we can free memory
updItems = null;
updIndex = 0;
return null;
}
}
/**
* Returns a SyncItem containing the key of the first/next
* deleted item of the store (locally removed after the last sync,
* but not yet deleted on server)
*/
public SyncItem getNextDeletedItem() throws SyncException {
if (delItems == null) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no deleted items to send");
}
return null;
}
if (delIndex<delItems.length) {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": sending item "
+delItems[delIndex].getKey());
}
// No need to get the content here
return delItems[delIndex++];
}
else {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Source "+getName()+": no more deletetd items to send");
}
// All Items sent, we can free memory
delItems = null;
delIndex = 0;
return null;
}
}
/**
* Tell the SyncSource the status returned by the server
* for an Item previously sent.
* This is a dummy implementation that just logs the status.
* A concrete implementation can override this method to perform
* some checks on the received status.
*
* @param key the key of the item
* @param status the status code received for that item
*
* @throws SyncException if the SyncSource wants to stop the sync
*/
protected void setItemStatus(String key, int status)
throws SyncException {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Status " + status + "for item " + key + "from server.");
}
}
/**
* Return the number of changes that the client will send during the
* session. This method, after the beginSync() call, should return
* the number of items to be sent to the server.
*
* The number of changes is computed by initXXXItems() during beginSync().
*
* @return number of items to sent, or -1 if unknown
*/
public int getClientItemsNumber() {
return clientItemsNumber;
}
/**
* Return the number of new items (add) that the client will send during the
* session. This method, after the beginSync() call, should return
* the number of new items to be sent to the server.
*
* The number of changes is computed by initXXXItems() during beginSync().
*
* @return number of items to sent, or -1 if unknown
*/
public int getClientAddNumber() {
return clientAddItemsNumber;
}
/**
* Return the number of replaced items that the client will send during the
* session. This method, after the beginSync() call, should return
* the number of replaced items to be sent to the server.
*
* The number of changes is computed by initXXXItems() during beginSync().
*
* @return number of items to sent, or -1 if unknown
*/
public int getClientReplaceNumber() {
return clientReplaceItemsNumber;
}
/**
* Return the number of deleted items that the client will send during the
* session. This method, after the beginSync() call, should return
* the number of delted items to be sent to the server.
*
* The number of changes is computed by initXXXItems() during beginSync().
*
* @return number of items to sent, or -1 if unknown
*/
public int getClientDeleteNumber() {
return clientDeleteItemsNumber;
}
/**
* Return the number of changes that the server will send during the
* session. This method, after the beginSync() call, should return
* the number of items to be sent to the server.
*
* @return number of changes from the server, or -1 if not announced.
*/
public int getServerItemsNumber() {
return serverItemsNumber;
}
/**
* Set the number of changes that the server will send during the
* session. This method is called by the engine to notify the Source
* of the number of changes announced by the server. If the server
* does not announce the number of changes, the engine will call
* this method with parameter -1.
*
* @param number of changes from the server, or -1 if not announced.
*/
public void setServerItemsNumber(int number) {
serverItemsNumber = number;
}
/**
* Return the Last Anchor for this source
*/
public SyncAnchor getSyncAnchor() {
return config.getSyncAnchor();
}
/**
* Set the value of the Last Anchor for this source
*/
public void setSyncAnchor(SyncAnchor anchor) {
config.setSyncAnchor(anchor);
}
/**
* Called after SyncManager preparation and initialization just before start
* the synchronization of the SyncSource.
*
* @param syncMode the synchronization type: one of the values in
* sync4j.framework.core.AlertCode
*
* @throws SyncException in case of error. This will stop the sync process
*/
public void beginSync(int syncMode, boolean resume) throws SyncException {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "Begin sync for source '" + getName() + "' with mode " + syncMode);
Log.info(TAG_LOG, "Resume = " + resume);
}
// Init lists
switch(syncMode) {
case FULL_SYNC:
case FULL_UPLOAD:
// A refresh from client is like a slow here
initAllItems();
allIndex = 0;
// Init number of changes counter
clientItemsNumber = (allItems != null) ? allItems.length : 0 ;
clientAddItemsNumber =
(newItems != null) ? newItems.length : 0 ;
clientReplaceItemsNumber =
(updItems != null) ? updItems.length : 0 ;
clientDeleteItemsNumber =
(delItems != null) ? delItems.length : 0 ;
break;
case INCREMENTAL_SYNC:
case INCREMENTAL_UPLOAD:
// A one way from client is like a fast here
initNewItems();
initUpdItems();
initDelItems();
newIndex = updIndex = delIndex = 0;
// Init number of changes counter
clientAddItemsNumber =
(newItems != null) ? newItems.length : 0 ;
clientReplaceItemsNumber =
(updItems != null) ? updItems.length : 0 ;
clientDeleteItemsNumber =
(delItems != null) ? delItems.length : 0 ;
clientItemsNumber = clientAddItemsNumber +
clientReplaceItemsNumber +
clientDeleteItemsNumber;
break;
case INCREMENTAL_DOWNLOAD:
// No modifications to send (it's not
// strictly necessary to reset the lists,
// because the engine will not ask items to
// the SyncSource, but it's good to do it)
newItems = null;
updItems = null;
delItems = null;
newIndex = updIndex = delIndex = 0;
// Init number of changes counter
clientItemsNumber = 0;
clientAddItemsNumber = 0;
clientReplaceItemsNumber = 0;
clientDeleteItemsNumber = 0;
break;
case FULL_DOWNLOAD:
// In this case, the SyncSource should
// delete all the items in the database
// (possibly asking the user before that)
// No modifications to send.
newItems = null;
updItems = null;
delItems = null;
newIndex = updIndex = delIndex = 0;
// Init number of changes counter
clientItemsNumber = 0;
clientAddItemsNumber = 0;
clientReplaceItemsNumber = 0;
clientDeleteItemsNumber = 0;
break;
default:
throw new SyncException(SyncException.SERVER_ERROR,
"SyncSource "+getName()+
": invalid sync mode "+getSyncMode());
}
this.syncMode = syncMode;
}
/**
* Called just before committing the synchronization process by the
* SyncManager. The SyncSource can stop the commit phase raising an
* exception here.
*
* @throws SyncException in case of error, to stop the commit.
*/
public void endSync() throws SyncException {
if (Log.isLoggable(Log.INFO)) {
Log.info(TAG_LOG, "End sync for source " + getName());
}
// Release resources
allItems = newItems = updItems = delItems = null;
allIndex = newIndex = updIndex = delIndex = 0;
}
/**
* Set a sync listener.
*
* @param listener the listener or null to remove it
*/
public void setListener(SyncListener listener) {
this.listener = listener;
}
/**
* Returns the current listener (or null if not set)
*/
public SyncListener getListener() {
return listener;
}
/**
* Returns the status of the sync source. The status is encoded as a bit
* mask of the STATUS_* values
*/
public int getStatus() {
return globalStatus;
}
/**
* Creates a new SyncItem for the engine to store incoming items
*/
public SyncItem createSyncItem(String key, String type, char state,
String parent, long size) {
SyncItem item = new SyncItem(key, type, state, parent);
return item;
}
/* ----------------------------------------------------------------------
* The following methods must be implemented by the concrete
* implementation of BaseSyncSource to perform the real modification
* detection, based on the source type.
*/
/**
* In a concrete implementation, this function should search the database
* for all the items present and store their keys.
*
* @throws SyncException implementation can throw a SyncException
* to stop the sync on fatal errors.
*/
protected abstract void initAllItems() throws SyncException;
/**
* In a concrete implementation, this function should search the database
* for the new items present and store their keys.
*
* @throws SyncException implementation can throw a SyncException
* to stop the sync on fatal errors.
*/
protected abstract void initNewItems() throws SyncException;
/**
* In a real implementation, this function should search the database
* for the modified items present and store their keys.
* The policy to detect a change can vary from one source to another:
* from generating a CRC to keep the status in a field of the item in
* the backend database.
*
* @throws SyncException implementation can throw a SyncException
* to stop the sync on fatal errors.
*/
protected abstract void initUpdItems() throws SyncException ;
/**
* In a real implementation, this function should search the database
* for the deleted items present and store their keys.
* The policy to detect a deleted item can vary from one source to another:
* from keeping a list of items after the last sync to keep the items with
* a deleted flag and then remove them after the successful deletion on
* the server.
*
* @throws SyncException implementation can throw a SyncException
* to stop the sync on fatal errors.
*/
protected abstract void initDelItems() throws SyncException ;
/**
* This function gets the item content in the backend database and
* returns a complete item. The parameter item is marked final because
* should not be used for the filled item: it is a reference to the
* array entry, and filling it would cause the array to keep all the
* filled items at the end (the gc will not dispose them). <p>
* The content of the item depends also from the encoding of this
* SyncSource:
* <li> if the encoding is <i>none</i>, it must be a String, converted
* with getBytes(), so the engine will send it unchanged.
* <li> if the encoding is <i>b64</i>, the content can be binary, and the
* type should be set accordingly, so that the receiving source
* can handle it. In this way, the binary content is transferred
* encoded in the SyncML message. This encoding can be applied to
* a test item too, to avoid problems with charset or other, like
* what is done with the SIF format.
*/
protected abstract SyncItem getItemContent(final SyncItem item)
throws SyncException ;
/**
* Indicates if the source supports resuming. If it does, then the methods
* <i>exists</i> and <i>hasChangedSinceLastSync</i> must be properly
* implemented.
*/
public boolean supportsResume() {
return false;
}
public boolean exists(String key) {
return true;
}
public boolean hasChangedSinceLastSync(String key, long ts) {
return true;
}
}