/* * 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.syncml.spds; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import com.funambol.util.MD5; import com.funambol.util.Base64; import com.funambol.util.StringUtil; import com.funambol.util.Log; import com.funambol.sync.SyncItem; import com.funambol.sync.SyncSource; import com.funambol.sync.ResumableSource; import com.funambol.sync.SyncListener; import com.funambol.sync.BasicSyncListener; import com.funambol.sync.SyncException; import com.funambol.sync.SyncConfig; import com.funambol.sync.SourceConfig; import com.funambol.sync.ItemStatus; import com.funambol.sync.SyncManagerI; import com.funambol.syncml.protocol.*; import com.funambol.util.HttpTransportAgent; import com.funambol.util.TransportAgent; import com.funambol.util.CodedException; /** * The SyncManager is the engine of the synchronization process on the * client library. It initializes the sync, checks the server responses * and communicate with the SyncSource, which is the client-specific * source of data. * A client developer must prepare a SyncConfig to instantiate a * SyncManager, and then can sync its sources calling the sync() * method. * By default the SyncManager uses an HttpTransportAgent to communicate with the * server, but the interface allows clients to specify their own transport * agent. */ public class SyncManager implements SyncManagerI { //------------------------------------------------------------- Private data private static final String TAG_LOG = "SyncManager"; /* Fast sync sending add state*/ private static final int STATE_SENDING_ADD = 1; /* Fast sync sending update state*/ private static final int STATE_SENDING_REPLACE = 2; /* Fast sync sending delete state*/ private static final int STATE_SENDING_DELETE = 3; /* Fast sync modification complete state*/ private static final int STATE_MODIFICATION_COMPLETED = 4; /* The current SyncML message must be flushed */ private static final int STATE_FLUSHING_MSG = 5; /* These are average sizes of messages used to estimate the msg size * before the message is actually formatted **/ private static final int SYNCML_XML_HDR_SIZE = 450; private static final int SYNCML_WBXML_HDR_SIZE = 250; private static final int SYNCML_XML_STATUS_SIZE = 140; private static final int SYNCML_WBXML_STATUS_SIZE = 100; private static final int SYNCML_XML_MAP_SIZE = 120; private static final int SYNCML_WBXML_MAP_SIZE = 80; /* SyncManager configuration*/ private SyncConfig config; private DeviceConfig deviceConfig; /* SyncSource to sync*/ protected SyncSource source; /* Device ID taken from DeviceConfig*/ private String deviceId; /* Max SyncML Message Size taken from DeviceConfig*/ protected int maxMsgSize; /** used to store the command ID where DevInf are sent to the server */ private String devIntPutCmdID; /** * A flag indicating if the client has to prepare the <DevInf> part of the * initialization SyncML message containing the device capabilities. It can * be set to <code>true</code> in two falls: * * a) the <code>serverUrl</code> isn't on the list of the already * connected servers * * b) the device configuration is changed */ private boolean forceSendDevInf = false; /** * A value indicating if the client has to add the device capabilities to the * modification message as content of a <Results> element. This occurs when * the server has sent a <Get> command request, sollicitating items of type * './devinf12'. * The value stored in this field is the cmd id of the get command or -1 if * no get command was sent by the server. */ private String addDevInfResults = null; // state used for fast sync int state; // The alerts sent by server, indexed by source name, instantiated in // checkServerAlerts private Hashtable serverAlerts; // The alert code for the current source (i.e. the actual sync mode // eventually modified by ther server protected int alertCode; // Server URL modified with session id. protected String serverUrl; protected String sessionID = null; /** * This table is a helper to handle the hierarchy. It is the reverse table * of the mappings as it allows guid -> luid retrieval. * It is used when an item has the source parent set, to retrieve the * corresponding item in the client representation (luid) * There is a specific table because a reverse search in the mappings would * be too inefficient. */ private Hashtable hierarchy = null; protected SyncStatus syncStatus = null; /** * This member stores the Status commands to send back to the server * in the next message. It is modified at each item received, * and is cleared after the status are sent. */ protected Vector statusList = null; /** * This is the list of items to be processed. The engine collects all the items sent from the * server and then apply all the changes in one shot to the sync source. */ protected ItemsList itemsToProcess = null; /** * This is the list of status to be processed. The engine collects all the status sent from the * server and then apply them in one shot to the sync source. */ protected Vector statusToProcess = null; /** * This member is used to store the current message ID. * It is sent to the server in the MsgID tag. */ private int msgID = 0; /** * This member is used to store the current command ID. * It is sent to the server in the CmdID tag. */ private CmdId cmdID = new CmdId(0); /** * A single TransportAgent for all the operations * performed in this Sync Manager */ private TransportAgent transportAgent; /** * This member is used to indicate if the SyncManager is busy, that is * if a sync is on going (SyncManager supports only one synchronization * at a time, and requests are queued in the synchronized method sync */ private boolean busy; /** * 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; /** * Mapping manager instance */ private MappingManager mappingManager = null; /** * This is a wrapper to handle incomining large object for sources that do * not support it. */ protected SyncSourceLOHandler sourceLOHandler = 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 SyncMLFormatter formatter; private SyncMLParser parser; // Are we using wbxml? protected boolean wbxml; // Has this message a gloabal no resp? protected boolean globalNoResp = false; // Are we resuming a sync? protected boolean resume = false; // This flag controls the logging of wbxml binary messages. To be used only // during debugging private boolean logBinaryMessages = false; private boolean forceCapsInXml = false; private boolean sendSuspendOnCancel = false; private boolean suspendAlertSent = false; //------------------------------------------------------------- Constructors /** * SyncManager constructor * * @param conf is the configuration data filled by the client * */ public SyncManager(SyncConfig conf, DeviceConfig deviceConfig) { this.config = conf; this.deviceConfig = deviceConfig; this.source = null; // Cache device info this.deviceId = deviceConfig.getDevID(); this.maxMsgSize = deviceConfig.getMaxMsgSize(); this.state = 0; this.serverAlerts = null; this.alertCode = 0; this.busy = false; // status commands statusList = null; transportAgent = new HttpTransportAgent( config.syncUrl, config.userAgent, "UTF-8", conf.compress, conf.forceCookies, conf.proxyConfig); wbxml = deviceConfig.isWBXML(); if (wbxml) { transportAgent.setRequestContentType("application/vnd.syncml+wbxml"); } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Using wbxml=" + wbxml); } parser = new SyncMLParser(wbxml); formatter = new SyncMLFormatter(wbxml); } //----------------------------------------------------------- Public methods /** * Synchronizes synchronization 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); } /** * Synchronizes synchronization source, using the preferred sync * mode defined for that SyncSource. * * @param source the SyncSource to synchronize * @param askServerDevInf forces the sync to query for server device * information. The information is returned to the client via the * SyncListener (@see SyncListener.startSyncing) * * @throws SyncException * If an error occurs during synchronization * */ public void sync(SyncSource source, boolean askServerDevInf) throws SyncException { sync(source, source.getSyncMode(), askServerDevInf); } /** * Synchronizes synchronization source * * @param source the SyncSource to synchronize * @param syncMode the sync mode * @throws SyncException * If an error occurs during synchronization */ public synchronized void sync(SyncSource src, int syncMode) throws SyncException { sync(src, syncMode, false); } /** * Synchronizes synchronization source * * @param source the SyncSource to synchronize * @param syncMode the sync mode * @param askServerDevInf forces the sync to query for server device * information. The information is returned to the client via the * SyncListener (@see SyncListener.startSyncing) * * @throws SyncException * If an error occurs during synchronization */ public synchronized void sync(SyncSource src, int syncMode, boolean askServerDevInf) throws SyncException { // Translate the abstract sync mode into the corresponding syncml one syncMode = getSyncMLSyncMode(syncMode); busy = true; cancel = false; resume = false; suspendAlertSent = false; devIntPutCmdID = null; // Initialize the mapping message manager if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Creating Mapping Manager"); } mappingManager = new MappingManager(src.getName()); // Initialize the sync status syncStatus = new SyncStatus(src.getName()); try { syncStatus.load(); // Is there an interrupted sync? boolean supportsResume = (src instanceof ResumableSource); boolean readyToResume = false; if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Last sync was interrupted = " + syncStatus.getInterrupted()); Log.info(TAG_LOG, "Source resume support is = " + supportsResume); readyToResume = ((ResumableSource)src).readyToResume(); } if (supportsResume && readyToResume && syncStatus.getInterrupted()) { // The last sync was a slow sync and it was interrupted, we can try // to resume it boolean exchangePhase = syncStatus.getSentItemsCount() > 0 || syncStatus.getReceivedItemsCount() > 0; if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Number of sent items = " + syncStatus.getSentItemsCount()); Log.debug(TAG_LOG, "Number of received items = " + syncStatus.getReceivedItemsCount()); } int interruptedSyncMode = syncStatus.getAlertedSyncMode(); if (exchangePhase && interruptedSyncMode == SyncML.ALERT_CODE_SLOW) { syncMode = SyncML.ALERT_CODE_RESUME; resume = true; if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Resuming interrupted session: " + syncStatus.getSessionId()); } } } // If the previous sync was not interrupted then we clear the status if (!syncStatus.getInterrupted()) { syncStatus.reset(); } } catch (Exception e) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "No sync status found for source: " + src.getName()); } } // We set the sync got interrupted so that if the application dies or // crashes for any reason, we know a sync was interrupted. // This flag is unset at the end of a successfull sync. syncStatus.setInterrupted(true); // Creates a sync source large object handler sourceLOHandler = new SyncSourceLOHandler(src, maxMsgSize, wbxml); sourceLOHandler.setSendSuspendOnCancel(sendSuspendOnCancel); // Update the sync status (do not save as it is not very useful so far) syncStatus.setRequestedSyncMode(syncMode); syncStatus.setLocUri(src.getName()); syncStatus.setRemoteUri(src.getSourceUri()); // By default the sync does not keep track of the hierarchy, we do it // only for slow and refresh from server. The hashtable is re-initialized // below, once the sync mode has been determined hierarchy = null; // Initialize the basicSyncListener if (basicListener == null) { basicListener = new BasicSyncListener(); } // Notifies the listener that a new sync is about to start getSyncListenerFromSource(src).startSession(); if (syncMode == SyncML.ALERT_CODE_NONE) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Source not active."); } syncStatus.setStatusCode(SyncListener.SUCCESS); getSyncListenerFromSource(src).endSession(syncStatus); return; } int syncStatusCode = SyncListener.SUCCESS; try { byte response[] = null; // Set source attribute this.source = src; // Set initial state nextState(STATE_SENDING_ADD); //Set NEXT Anchor referring to current timestamp long syncStartTime = System.currentTimeMillis(); SyncMLAnchor anchor = (SyncMLAnchor)this.source.getSyncAnchor(); anchor.setNext(syncStartTime); syncStatus.setStartTime(syncStartTime); this.sessionID = String.valueOf(System.currentTimeMillis()); this.serverUrl = config.syncUrl; // init status commands list this.statusList = new Vector(); // ================================================================ // Initialization phase // ================================================================ // Update the sync status syncStatus.setSessionId(sessionID); syncStatus.setLastSyncStartTime(syncStartTime); saveSyncStatus(); DevInf devInf = performInitializationPhase(syncMode, askServerDevInf, syncStatus); // ================================================================ // Sync phase // ================================================================ if (isSyncToBeCancelled()) { cancelSync(); } // Inform the handler about the actual sync mode sourceLOHandler.setResume(resume); // The hierarchy needs to be initialized for any sync switch (alertCode) { case SyncML.ALERT_CODE_FAST: case SyncML.ALERT_CODE_TWO_WAY_BY_SERVER: case SyncML.ALERT_CODE_ONE_WAY_FROM_SERVER_BY_SERVER: case SyncML.ALERT_CODE_ONE_WAY_FROM_SERVER: hierarchy = mappingManager.getMappings("hierarchy-" + source.getName()); break; case SyncML.ALERT_CODE_SLOW: case SyncML.ALERT_CODE_NONE: case SyncML.ALERT_CODE_REFRESH_FROM_SERVER_BY_SERVER: case SyncML.ALERT_CODE_REFRESH_FROM_SERVER: case SyncML.ALERT_CODE_REFRESH_FROM_CLIENT_BY_SERVER: case SyncML.ALERT_CODE_REFRESH_FROM_CLIENT: case SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT_BY_SERVER: case SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT: case SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT_NO_SLOW: // We start from scratch because we don't support slow sync // resuming. Otherwise we should keep the hierarchy around hierarchy = new Hashtable(); mappingManager.resetMappings("hierarchy-" + source.getName()); break; default: break; } // Prepopulate the hierarchy with the root item, whose // mapping is itself if (hierarchy.get("/") == null) { hierarchy.put("/", "/"); } // Notifies that the synchronization is going to begin boolean ok = getSyncListenerFromSource(src).startSyncing(alertCode, devInf); if (!ok) { //User Aborts the slow sync request if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Sync process aborted by the user"); } syncStatusCode = SyncListener.CANCELLED; return; } int actualSyncMode; if (resume) { // In case of resume, the alert code is the alert code of the // interrupted sync actualSyncMode = syncStatus.getAlertedSyncMode(); source.beginSync(getSourceSyncMode(actualSyncMode), true); } else { actualSyncMode = alertCode; source.beginSync(getSourceSyncMode(alertCode), false); } getSyncListenerFromSource(src).syncStarted(alertCode); boolean done = false; // the implementation of the client/server multi-messaging // through a do while loop: while </final> tag is reached. if (actualSyncMode == SyncML.ALERT_CODE_SLOW || actualSyncMode == SyncML.ALERT_CODE_REFRESH_FROM_CLIENT) { getSyncListenerFromSource(src).startSending(0, source.getClientItemsNumber(), 0); } else { getSyncListenerFromSource(src).startSending(source.getClientAddNumber(), source.getClientReplaceNumber(), source.getClientDeleteNumber()); } do { byte modificationsMsg[] = prepareModificationMessage(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Sending modification"); } // Release the memory pool. All the objects that were part of // the sent message are discarded ObjectsPool.release(); if (isSyncToBeCancelled()) { cancelSync(); } response = postRequest(modificationsMsg); if (wbxml) { logBinaryMessage(response); } modificationsMsg = null; // Update the sync status since we are sure a bunch of mappings // were sent Hashtable mappings = syncStatus.getPendingMappings(); Enumeration mapKeys = mappings.keys(); while(mapKeys.hasMoreElements()) { String luid = (String)mapKeys.nextElement(); syncStatus.addMappingSent(luid); } saveSyncStatus(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Response received"); } logMessage(response, false); getSyncListenerFromSource(src).endSending(); // The startReceiving(n) is notified from within the // processModifications because here we do not know the number // of messages to be received SyncML msg = parser.parse(response); response = null; // Note that we cannot release the pool here, as during the // parsing we prepare the status to be used during the next // message preparation getSyncListenerFromSource(src).endSending(); done = processModifications(msg, source); getSyncListenerFromSource(src).endReceiving(); // Now we can throw the exception to cancel the sync if (cancel && sendSuspendOnCancel && suspendAlertSent) { throw new SyncException(SyncException.CANCELLED, "SyncManager sync got cancelled"); } } while (!done); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Modification session succesfully completed"); } getSyncListenerFromSource(src).endSyncing(); // ================================================================ // Mapping phase // ================================================================ if (isSyncToBeCancelled()) { cancelSync(); } getSyncListenerFromSource(src).startFinalizing(); // Send the map message only if a mapping or a status has to be sent Hashtable mappings = syncStatus.getPendingMappings(); if (statusList.size() > 0 || mappings.size() > 0) { byte mapMsg[] = prepareMappingMessage(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Sending Mappings\n"); } try { response = postRequest(mapMsg); if (wbxml) { logBinaryMessage(response); } // Update the sync status since we are sure a bunch of mappings // were sent Enumeration mapKeys = mappings.keys(); while(mapKeys.hasMoreElements()) { String luid = (String)mapKeys.nextElement(); syncStatus.addMappingSent(luid); } saveSyncStatus(); // Release the memory pool ObjectsPool.release(); } catch (ReadResponseException rre) { SyncMLAnchor syncMLAnchor = (SyncMLAnchor)source.getSyncAnchor(); syncMLAnchor.setLast(syncMLAnchor.getNext()); //save last anchors if the mapping message has been sent but //the response has not been received due to network problems if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Last sync message sent - Error reading the response " + rre); } } mapMsg = null; if (response != null) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Response received"); } logMessage(response, false); SyncML msg = parser.parse(response); response = null; processMapResponse(msg); } else { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Response not received, skipping check for status"); } } if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Mapping session succesfully completed"); } } else { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "No mapping message to send"); } } // TODO: following code must be run only for successfull path or error reading inputstream // the other cases must skip the following code if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Notifying listener end mapping"); } getSyncListenerFromSource(src).endFinalizing(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Changing anchors"); } // Set the last anchor to the next timestamp for the source SyncMLAnchor syncMLAnchor = (SyncMLAnchor)source.getSyncAnchor(); syncMLAnchor.setLast(syncMLAnchor.getNext()); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Ending session (" + syncStatusCode + ")"); } // Tell the source that the sync is finished if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Calling source endSync"); } source.endSync(); // If the synchronization terminates with no errors, then we reset // the hierarchy, because the sync is really over and it cannot be // resumed mappingManager.resetMappings("hierarchy-" + source.getName()); // This sync terminated syncStatus.setInterrupted(false); // Create a listener status from the source status syncStatusCode = getListenerStatusFromSourceStatus(source.getStatus()); } catch (CompressedSyncException compressedSyncException) { Log.error(TAG_LOG, "CompressedSyncException: ", compressedSyncException); syncStatusCode = SyncListener.COMPRESSED_RESPONSE_ERROR; throw compressedSyncException; } catch (SyncException se) { Log.error(TAG_LOG, "SyncException", se); // Create a listener status from the exception syncStatusCode = getListenerStatusFromSyncException(se); throw se; } catch (Throwable e) { if (e instanceof SecurityException) { Log.error(TAG_LOG, "Security Exception", e); throw (SecurityException)e; } else { Log.error(TAG_LOG, "Exception", e); syncStatusCode = SyncListener.GENERIC_ERROR; throw new SyncException(SyncException.CLIENT_ERROR, e.toString()); } } finally { // Regardless of how we got here, we save the sync status to be sure // it is persisted syncStatus.setStatusCode(syncStatusCode); // Save the sync end time long syncEndTime = System.currentTimeMillis(); syncStatus.setEndTime(syncEndTime); // Persist the status saveSyncStatus(); // Notifies the listener that the session is over if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Ending session (" + syncStatusCode + ")"); } // Notify the listener that the sync is finished try { getSyncListenerFromSource(src).endSession(syncStatus); } finally { releaseResources(); sourceLOHandler.releaseResources(); } } } /** * This method cancels the current sync. The sync is interrupted once the * engine reaches the next check point (in other words the termination is * not immediate). * When a sync is interrupted, a SyncException with code CANCELLED is * thrown. This exception will be thrown by the thread running the * synchronization itself, not by this method. * This method is non blocking, it marks the sync as to be cancelled and * returns, without waiting for the sync to be really cancelled. * If this SyncManager is performing more syncs cuncurrently, then all of * them are cancelled. */ public void cancel() { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Cancelling sync in manager"); } cancel = true; if (sourceLOHandler != null) { sourceLOHandler.cancel(); } } /** * To be invoked by every change of the device configuration and if the * serverUrl is a new one (i.e., not already on the list * <code>lastServerUrl</code> */ public void setFlagSendDevInf() { forceSendDevInf = true; } /** * Checks if the manager is currently busy performing a synchronization. * @return true if a sync is currently on going */ public boolean isBusy() { return busy; } /** * Sets the transport agent to be used for the next message to be sent. If * this method is invoked in the middle of a sync, it changes the connection * method for that very sync. This is a possible behavior, but it is very * uncommon. Users should make sure no sync is in progress when the * transport agent is changed. Typically the transport agent is changed * before the first sync and not changed afterward. * @param ta the transport agent * @throws IllegalArgumentException if the give transport agent is null */ public void setTransportAgent(TransportAgent ta) { if (ta != null) { transportAgent = ta; } else { throw new IllegalArgumentException("Transport agent cannot be null"); } } /** * This method allows clients to add custom headers into HTTP requests */ public void addTranportAgentHeaders(Hashtable headers) { if (transportAgent != null) { transportAgent.setCustomHeaders(headers); } } /** * This method returns the sync report of the last sync performed. This * information is valid until a new sync is fired. */ public SyncStatus getSyncStatus() { return syncStatus; } public void setForceCapsInXml(boolean value) { this.forceCapsInXml = value; } //---------------------------------------------------------- Private methods private DevInf performInitializationPhase(int syncMode, boolean askServerDevInf, SyncStatus syncStatus) throws SyncException, SyncMLParserException { // Get ready to try the authentication more than once, because it can // fail for invalid nonce or invalid auth method and we must perform // different attempts boolean md5 = SyncConfig.AUTH_TYPE_MD5 == config.preferredAuthType; int md5Attempts = 0; boolean retry; // Reset the msgId here resetMsgID(); getSyncListenerFromSource(source).startConnecting(); do { retry = false; // For efficiency we clear the status list statusList.removeAllElements(); if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Sending init message " + md5); } //find device capabilities hash DevInf devInf = null; devInf = createDevInf(deviceConfig, source); String newDevInfHash = null; //check, for a SyncML sync, if the device capabilities //must be sent to the server if (source.getConfig() instanceof SyncMLSourceConfig) { String xmlDevInfPlain = null; try { StringBuffer sb = new StringBuffer(createXmlDevInf(devInf)); //hash depends on devinf, server url and username sb.append("-") .append(config.getSyncUrl()) .append("-") .append(config.getUserName()); //calculate MD5 xmlDevInfPlain = sb.toString(); } catch (IOException ioe) { String msg = "Cannot prepare output message: " + ioe.toString(); Log.error(TAG_LOG, msg); throw new SyncException(SyncException.CLIENT_ERROR, msg); } try { byte[] newDevInfHashBytes = new MD5().calculateMD5(xmlDevInfPlain.getBytes("UTF-8")); newDevInfHash = new String(newDevInfHashBytes); } catch (UnsupportedEncodingException e) { //really, not a big problem newDevInfHash = ""; } SyncMLSourceConfig syncMLSourceConfig = (SyncMLSourceConfig)source.getConfig(); String lastDevInfHash = syncMLSourceConfig.getLastDevInfHash(); if (!forceSendDevInf && newDevInfHash.equals(lastDevInfHash)) { //doesn't send DevInf devInf = null; } } //Format request message to be sent to the server byte initMsg[] = prepareInitializationMessage( syncMode, devInf, askServerDevInf, md5); if (isSyncToBeCancelled()) { cancelSync(); } if (wbxml) { logBinaryMessage(initMsg); logMessage(initMsg, false); } byte response[] = postRequest(initMsg); initMsg = null; logMessage(response, false); if (wbxml) { logBinaryMessage(response); } // Release the memory pool ObjectsPool.release(); SyncML syncMLMsg = parser.parse(response); try { DevInf serverDevInf = processInitMessage(syncMLMsg, source, newDevInfHash); // If we asked for server caps but did not get them, then we throw an // exception if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Response received"); } logMessage(response, false); if (askServerDevInf && serverDevInf == null) { Log.error(TAG_LOG, "Server did not send requested capabilities"); // TODO: the server could return a 204 status. In such a // case we should not throw an exception throw new SyncException(SyncException.SERVER_ERROR, "Cannot find server capabilities in server response"); } return serverDevInf; } catch (AuthenticationException ae) { // Handle authentication errors and retries String authMethod = ae.getAuthMethod(); String nextNonce = ae.getNextNonce(); if (SyncML.AUTH_TYPE_MD5.equals(authMethod)) { // The server required md5 authentication if (config.allowMD5Authentication()) { // If the previous attempt was not md5 or // the previous was md5 but the first try, then // we try again with the new nonce if (!md5 || (md5 && md5Attempts == 0)) { // Try again with the new nonce if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Setting next nonce to " + nextNonce); } retry = true; md5 = true; } } if (nextNonce != null) { config.clientNonce=nextNonce; } } else if (SyncML.AUTH_TYPE_BASIC.equals(authMethod) && md5) { // The previous md5 auth failed and the server required a // basic auth. If the client allows it, we fall back to // basic if (config.allowBasicAuthentication()) { retry = true; md5 = false; } } if (!retry) { throw new SyncException(SyncException.AUTH_ERROR, "Invalid credentials"); } } if (md5) { md5Attempts++; } } while(retry); // If we get here, we could not authenticate successfully throw new SyncException(SyncException.CLIENT_ERROR, "Cannot authenticate"); } protected DevInf processInitMessage( SyncML message, SyncSource source, String newDevInfHash) throws SyncException { String sourceName = source.getName(); SyncBody body = message.getSyncBody(); DevInf serverDevInf = null; // Process the header SyncHdr hdr = message.getSyncHdr(); if (hdr != null) { String respURI = hdr.getRespURI(); if (respURI != null) { serverUrl = respURI; if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Found respURI = " + serverUrl); } } globalNoResp = hasNoResp(hdr.getNoResp()); Meta metaHdr = hdr.getMeta(); if (metaHdr != null && metaHdr.getMaxMsgSize() != null) { Long maxMsgSizeValue = metaHdr.getMaxMsgSize(); if (maxMsgSizeValue != null) { try { int serverMaxMsgSize = (int)maxMsgSizeValue.longValue(); // If the server max msg size is smaller than the our // one then we use it if (serverMaxMsgSize < maxMsgSize) { maxMsgSize = serverMaxMsgSize; if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Reducing maxMsgSize according to server request " + maxMsgSize); } } } catch (Exception e) { // In this case we just ignore the error Log.error(TAG_LOG, "Cannot parse max msg size sent from server", e); } } } } if (!globalNoResp) { Status hdrStatus = Status.newInstance(); hdrStatus.setMsgRef(""+msgID); hdrStatus.setCmdRef("0"); hdrStatus.setCmd(SyncML.TAG_SYNCHDR); TargetRef tr = TargetRef.newInstance(); tr.setValue(deviceId); hdrStatus.setTargetRef(tr); SourceRef sr = SourceRef.newInstance(); sr.setValue(serverUrl); hdrStatus.setSourceRef(sr); hdrStatus.setData(Data.newInstance("" + SyncMLStatus.SUCCESS)); // Add this status to the list statusList.addElement(hdrStatus); } // Process the body if (body == null) { return null; } Vector commands = body.getCommands(); // Process all the commands and verify the status codes for the header // and the alert boolean hdrStatus = false; boolean alertStatus = false; for(int i=0;i<commands.size();++i) { Object command = commands.elementAt(i); if (command instanceof Alert) { Alert alert = (Alert)command; // Iterate over the items, looking for the one whose target is // the source name Vector items = alert.getItems(); String serverNextAnchor = null; for(int j=0;j<items.size();++j) { Item item = (Item)items.elementAt(j); Target target = item.getTarget(); if (target != null) { String locURI = target.getLocURI(); if (sourceName.equals(locURI)) { alertCode = alert.getData(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Found alert tag " + alertCode); } // The server may send its anchor here. We need to // store to generate the proper status Meta alertMeta = item.getMeta(); if (alertMeta != null) { Anchor anchor = alertMeta.getAnchor(); if (anchor != null) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Server next anchor is: " + anchor.getNext()); } serverNextAnchor = anchor.getNext(); } } // Update the sync status syncStatus.setAlertedSyncMode(alertCode); saveSyncStatus(); break; } } } if (!globalNoResp) { // Prepare a status for the alert Status aStatus = Status.newInstance(); aStatus.setMsgRef("" + 1); aStatus.setCmdRef(alert.getCmdID()); aStatus.setCmd(SyncML.TAG_ALERT); TargetRef tr = TargetRef.newInstance(); tr.setValue(source.getSourceUri()); aStatus.setTargetRef(tr); SourceRef sr = SourceRef.newInstance(); sr.setValue(source.getName()); aStatus.setSourceRef(sr); aStatus.setData(Data.newInstance(""+SyncMLStatus.SUCCESS)); Item alertStatusItem = Item.newInstance(); Anchor nAnchor = new Anchor(); if (serverNextAnchor == null) { SyncMLAnchor anchor = (SyncMLAnchor)source.getSyncAnchor(); long nextAnchor = anchor.getNext(); nAnchor.setNext(""+nextAnchor); } else { nAnchor.setNext(serverNextAnchor); } alertStatusItem.setData(Data.newInstance(nAnchor)); aStatus.setItem(alertStatusItem); // Add the status to the list statusList.addElement(aStatus); } } else if (command instanceof Status) { Status status = (Status)command; String cmd = status.getCmd(); if (SyncML.TAG_SYNCHDR.equals(cmd)) { checkStatusCode(status); hdrStatus = true; } else if (SyncML.TAG_ALERT.equals(cmd)) { checkStatusCode(status); int statusCode = getStatusCode(status); int syncMode = syncStatus.getRequestedSyncMode(); if (statusCode == SyncMLStatus.REFRESH_REQUIRED && syncMode == SyncML.ALERT_CODE_RESUME) { // The server refused our resume attempt. At this point // we can wipe the SyncStatus info if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Server refused to resume. Wiping old sync status"); } try { //long lastSyncTimeStamp = syncStatus.getLastSyncStartTime(); //String locUri = syncStatus.getLocUri(); syncStatus.resetExchangedItems(); syncStatus.setSessionId(sessionID); syncStatus.setRequestedSyncMode(syncMode); syncStatus.setInterrupted(true); //syncStatus.setLastSyncStartTime(lastSyncTimeStamp); saveSyncStatus(); resume = false; } catch (Exception e) { Log.error(TAG_LOG, "Cannot reset sync status", e); throw new SyncException(SyncException.CLIENT_ERROR, "Cannot reset sync status"); } } alertStatus = true; } else if (SyncML.TAG_PUT.equals(cmd)) { //check for DevInf from client to server, only needed for some kind of SyncSource objects if (source.getConfig() instanceof SyncMLSourceConfig) { int code = getStatusCode(status); String cmdRef = status.getCmdRef(); if (!StringUtil.isNullOrEmpty(devIntPutCmdID) && devIntPutCmdID.equalsIgnoreCase(cmdRef)) { Log.trace(TAG_LOG, "Server DevInf from client status: " + code); if (SyncMLStatus.SUCCESS == code) { //check for correct reply in correct task if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Updating last dev inf hash to " + newDevInfHash); } SyncMLSourceConfig syncMLSourceConfig = (SyncMLSourceConfig)source.getConfig(); syncMLSourceConfig.setLastDevInfHash(newDevInfHash); //somewhere, over the rainbow, the configuration will be saved } //just in case ;) devIntPutCmdID = null; } } } } else if (command instanceof Get) { // TODO Today we only support dev cap get addDevInfResults = checkIfServerRequiredDevInf((Get)command); } else if (command instanceof Results) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Found Results command"); } // These are the results of the get commands we sent Results results = (Results)command; Vector items = results.getItems(); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Number of items: " + items.size()); } for(int j=0;j<items.size();++j) { Object item = items.elementAt(j); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "item=" + item); } // TODO: we shall check the cmdref to make sure this is what // we are looking for if (item instanceof DevInfItem) { DevInfItem devInfItem = (DevInfItem)item; serverDevInf = devInfItem.getDevInf(); } } } } // If we did not receive the status(es) we expected, then we throw an // error if (!hdrStatus || !alertStatus) { String msg = "Status code from server not received "; Log.error(TAG_LOG, msg + " hdr=" + hdrStatus + " alert=" + alertStatus); throw new SyncException(SyncException.SERVER_ERROR, msg); } return serverDevInf; } /** * Posts the given message to the url specified by <code>serverUrl</code>. * * @param request the request msg * @return the response of the server as a string * * @throws SyncException in case of network errors (thrown by sendMessage) */ private byte[] postRequest(byte request[]) throws SyncException { transportAgent.setRequestURL(serverUrl); try { return transportAgent.sendMessage(request); } catch (CodedException ce) { int code; switch (ce.getCode()) { case CodedException.DATA_NULL: code = SyncException.DATA_NULL; break; case CodedException.CONN_NOT_FOUND: code = SyncException.CONN_NOT_FOUND; break; case CodedException.ILLEGAL_ARGUMENT: code = SyncException.ILLEGAL_ARGUMENT; break; case CodedException.WRITE_SERVER_REQUEST_ERROR: code = SyncException.WRITE_SERVER_REQUEST_ERROR; WriteRequestException wre = new WriteRequestException(code, ce.toString()); throw wre; case CodedException.ERR_READING_COMPRESSED_DATA: CompressedSyncException cse = new CompressedSyncException(ce.toString()); throw cse; case CodedException.CONNECTION_BLOCKED_BY_USER: code = SyncException.CONNECTION_BLOCKED_BY_USER; break; case CodedException.READ_SERVER_RESPONSE_ERROR: code = SyncException.READ_SERVER_RESPONSE_ERROR; ReadResponseException rre = new ReadResponseException(code, ce.toString()); throw rre; case CodedException.OPERATION_INTERRUPTED: code = SyncException.CANCELLED; break; default: code = SyncException.CLIENT_ERROR; break; } SyncException se = new SyncException(code, ce.toString()); throw se; } } private int getStatusCode(Status status) throws SyncException { Data data = status.getData(); if (data == null) { String msg = "Status from server has no data"; Log.error(TAG_LOG, msg); throw new SyncException(SyncException.SERVER_ERROR, msg); } String codeVal = data.getData(); try { int code = Integer.parseInt(codeVal); return code; } catch (Exception e) { String msg = "Status code from server is not a valid number " + codeVal; Log.error(TAG_LOG, msg); throw new SyncException(SyncException.SERVER_ERROR, msg); } } private void checkStatusCode(Status status) throws SyncException { int code = getStatusCode(status); Data data = status.getData(); String msg = data.getData(); switch (code) { case SyncMLStatus.SUCCESS: // 200 return; case SyncMLStatus.REFRESH_REQUIRED: // 508 if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Refresh required by server."); } return; case SyncMLStatus.AUTHENTICATION_ACCEPTED: // 212 { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Authentication accepted by the server"); } Chal chal = status.getChal(); if (chal != null) { NextNonce nextNonce = chal.getNextNonce(); if (nextNonce != null) { // Save the new nonce if the server sent it if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Saving next nonce"); } config.clientNonce = new String(nextNonce.getValue()); } } return; } case SyncMLStatus.INVALID_CREDENTIALS: // 401 { Log.error(TAG_LOG, "Invalid credentials: " + config.userName); // Grab the authentication chal info and propagate // it String nextNonce = null; String authMethod = null; String nonceFormat = null; Chal chal = status.getChal(); if (chal != null) { NextNonce nNonce = chal.getNextNonce(); nonceFormat = chal.getFormat(); authMethod = chal.getType(); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Required auth method: " + authMethod); } if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Nonce format " + nonceFormat); } if (nNonce != null) { // Save the new nonce if the server sent it nextNonce = nNonce.getValue(); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Saving next nonce " + nextNonce); } } } if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Server required authentication " + authMethod + " and nonce: " + nextNonce); } AuthenticationException authExc = new AuthenticationException("Authentication failed", authMethod, nonceFormat, nextNonce); throw authExc; } case SyncMLStatus.FORBIDDEN: // 403 throw new SyncException( //SyncException.AUTH_ERROR, SyncException.FORBIDDEN_ERROR, "User not authorized: " + config.userName + " for source: " + source.getSourceUri()); case SyncMLStatus.NOT_FOUND: // 404 Log.error(TAG_LOG, "Source URI not found on server: " + source.getSourceUri()); throw new SyncException( //SyncException.ACCESS_ERROR, SyncException.NOT_FOUND_URI_ERROR, "Source URI not found on server: " + source.getSourceUri()); case SyncMLStatus.SERVER_BUSY: // 503 throw new SyncException( SyncException.SERVER_BUSY, "Server busy, another sync in progress for " + source.getSourceUri()); case SyncMLStatus.PROCESSING_ERROR: // 506 throw new SyncException( SyncException.BACKEND_ERROR, "Error processing source: " + source.getSourceUri() + "," + msg); case SyncMLStatus.BACKEND_AUTH_ERROR: // 511 throw new SyncException( SyncException.BACKEND_AUTH_ERROR, "Error processing source: " + source.getSourceUri() + "," + msg); default: // Unhandled status code if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Unhandled Status Code, throwing exception " + code); } throw new SyncException( SyncException.SERVER_ERROR, "Error from server: " + code); } } /** * Check if this Get command is for devinf12 (we do not support devinf11) * @param get the Get command * @return the cmdId of the get command for the device caps, null otherwise */ private String checkIfServerRequiredDevInf(Get get) { Vector items = get.getItems(); for(int i=0;i<items.size();++i) { Item item = (Item)items.elementAt(i); Target target = item.getTarget(); if (target != null) { String locURI = target.getLocURI(); if (SyncML.DEVINF12.equals(locURI)) { return get.getCmdID(); } } } return null; } protected byte[] prepareInitializationMessage(int syncMode, boolean requireDevInf, boolean md5Auth) throws SyncException { DevInf devInf = createDevInf(deviceConfig, source); return prepareInitializationMessage(syncMode, devInf, requireDevInf, md5Auth); } /** * Prepares inizialization SyncML message */ protected byte[] prepareInitializationMessage( int syncMode, DevInf devInf, boolean requireDevInf, boolean md5Auth) throws SyncException { try { SyncML msg = new SyncML(); // Prepare the header SyncHdr syncHdr = new SyncHdr(); // Prepare the credentials MetInf credMetInf = MetInf.newInstance(); String token; if (md5Auth) { credMetInf.setType(Cred.AUTH_TYPE_MD5); // Prepare a MD5 authentication tag MD5 md5Computer = new MD5(); String nonceB64 = config.clientNonce; byte nonce[]; if (nonceB64 == null) { nonce = "".getBytes(); } else { // The nonce in the config is b64 encoded // (in XML this is a MUST) nonce = Base64.decode(nonceB64.getBytes()); } if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Computing cred with nonce: " + nonceB64); } byte byteToken[] = md5Computer.computeMD5Credentials(config.userName, config.password, nonce); token = new String(byteToken); } else { credMetInf.setType(Cred.AUTH_TYPE_BASIC); // TODO FIXME: use a constant credMetInf.setFormat("b64"); String login = config.userName + ":" + config.password; token = new String(Base64.encode(login.getBytes())); } Meta credMeta = Meta.newInstance(); credMeta.setMetInf(credMetInf); Cred cred = new Cred(); cred.setMeta(credMeta); cred.setData(Data.newInstance(token)); syncHdr.setCred(cred); // Prepare the SyncHdr meta MetInf hdrMetInf = MetInf.newInstance(); hdrMetInf.setMaxMsgSize(new Long(maxMsgSize)); Meta hdrMeta = Meta.newInstance(); hdrMeta.setMetInf(hdrMetInf); syncHdr.setMeta(hdrMeta); // Prepare the VerDTD and VerProto tags VerDTD verDTD = new VerDTD("1.2"); syncHdr.setVerDTD(verDTD); syncHdr.setVerProto("SyncML/1.2"); // Set the session ID syncHdr.setSessionID(sessionID); // Set the message ID resetMsgID(); syncHdr.setMsgID(getNextMsgID()); // Set the source and the target Source hdrSource = Source.newInstance(); hdrSource.setLocURI(deviceId); hdrSource.setLocName(config.userName); syncHdr.setSource(hdrSource); Target hdrTarget = Target.newInstance(); hdrTarget.setLocURI(serverUrl); syncHdr.setTarget(hdrTarget); // Now create the sync header and add it to the msg msg.setSyncHdr(syncHdr); // Prepare the body SyncBody syncBody = new SyncBody(); SyncMLAnchor syncMLAnchor = (SyncMLAnchor)source.getSyncAnchor(); long nextAnchor = syncMLAnchor.getNext(); long lastAnchor = syncMLAnchor.getLast(); String sourceUri = source.getSourceUri(); String sourceName = source.getName(); // Prepare the alert resetCmdID(); Alert alert = new Alert(); alert.setCmdID(getNextCmdID()); alert.setData(syncMode); Item alertItem = Item.newInstance(); Source alertSource = Source.newInstance(); alertSource.setLocURI(sourceName); alertItem.setSource(alertSource); Target alertTarget = Target.newInstance(); // In a resume, the target shall specify the name of the remote // source if (syncMode == SyncML.ALERT_CODE_RESUME) { alertTarget.setLocURI(source.getConfig().getRemoteUri()); } else { alertTarget.setLocURI(sourceUri); } alertItem.setTarget(alertTarget); Meta alertItemMeta = Meta.newInstance(); Anchor anchor = new Anchor(); if (syncMode != SyncML.ALERT_CODE_RESUME) { anchor.setLast(""+lastAnchor); } anchor.setNext(""+nextAnchor); alertItemMeta.setAnchor(anchor); alertItem.setMeta(alertItemMeta); Vector alertItems = new Vector(1); alertItems.addElement(alertItem); alert.setItems(alertItems); Vector bodyCommands = new Vector(1); bodyCommands.addElement(alert); // Handle DevInf (both ways) // Add DevInf if we need to put them if (null != devInf) { Put devInfPut = new Put(); devIntPutCmdID = getNextCmdID(); devInfPut.setCmdID(devIntPutCmdID); Meta putMeta = Meta.newInstance(); devInfPut.setMeta(putMeta); Item putItem = Item.newInstance(); Source putItemSource = Source.newInstance(); putItemSource.setLocURI(SyncML.DEVINF12); putItem.setSource(putItemSource); if (wbxml && forceCapsInXml) { // We always send caps in xml, even if the sync is in wbxml putMeta.setType("application/vnd.syncml-devinf+xml"); String xmlDevInf = createXmlDevInf(devInf); putItem.setData(Data.newInstance(xmlDevInf)); } else if (!wbxml) { putMeta.setType("application/vnd.syncml-devinf+xml"); putItem.setData(Data.newInstance(devInf)); } else { putMeta.setType("application/vnd.syncml-devinf+wbxml"); putItem.setData(Data.newInstance(devInf)); } Vector putItems = new Vector(); putItems.addElement(putItem); devInfPut.setItems(putItems); bodyCommands.addElement(devInfPut); //reset the flag forceSendDevInf = false; } // Add a get command to query the server for its caps if (requireDevInf) { // We need to add a Get command for the server caps Get devInfGet = new Get(); devInfGet.setCmdID(getNextCmdID()); Meta getMeta = Meta.newInstance(); devInfGet.setMeta(getMeta); Item getItem = Item.newInstance(); Target getItemTarget = Target.newInstance(); getItemTarget.setLocURI(SyncML.DEVINF12); getItem.setTarget(getItemTarget); if (wbxml && forceCapsInXml) { // We always send caps in xml, even if the sync is in wbxml // This is because some servers do not understand wbxml caps getMeta.setType("application/vnd.syncml-devinf+xml"); } else if (!wbxml) { getMeta.setType("application/vnd.syncml-devinf+xml"); } else { getMeta.setType("application/vnd.syncml-devinf+wbxml"); } Vector getItems = new Vector(); getItems.addElement(getItem); devInfGet.setItems(getItems); bodyCommands.addElement(devInfGet); } syncBody.setCommands(bodyCommands); // This is the end of the init msg syncBody.setFinalMsg(new Boolean(true)); msg.setSyncBody(syncBody); logMessage(msg, false); ByteArrayOutputStream os = new ByteArrayOutputStream(); formatter.format(msg, os, "UTF-8"); return os.toByteArray(); } catch (IOException ioe) { String msg = "Cannot prepare output message: " + ioe.toString(); Log.error(TAG_LOG, msg); throw new SyncException(SyncException.CLIENT_ERROR, msg); } } private String createXmlDevInf(DevInf devInf) throws IOException { SyncMLFormatter xmlFormatter = new SyncMLFormatter(false); ByteArrayOutputStream os = new ByteArrayOutputStream(); xmlFormatter.formatXmlDevInf(devInf, os, "UTF-8"); return os.toString(); } private DevInf createDevInf(DeviceConfig deviceConfig, SyncSource source) { String sourceName = source.getName(); String sourceType = source.getType(); DevInf devInf = new DevInf(); devInf.setVerDTD(new VerDTD(deviceConfig.getVerDTD())); devInf.setMan(deviceConfig.getMan()); devInf.setMod(deviceConfig.getMod()); devInf.setOEM(deviceConfig.getOEM()); devInf.setSwV(deviceConfig.getSwV()); devInf.setFwV(deviceConfig.getFwV()); devInf.setHwV(deviceConfig.getHwV()); devInf.setDevID(deviceConfig.getDevID()); devInf.setDevTyp(deviceConfig.getDevType()); devInf.setUTC(new Boolean(deviceConfig.getUtc())); devInf.setSupportLargeObjs(new Boolean(deviceConfig.getLoSupport())); devInf.setSupportNumberOfChanges(new Boolean(deviceConfig.getNocSupport())); // The source can also provide Ext SourceConfig srcConfig = source.getConfig(); DataStore ds = null; if (srcConfig instanceof SyncMLSourceConfig) { SyncMLSourceConfig syncMLSrcConfig = (SyncMLSourceConfig)srcConfig; Vector devInfExts = syncMLSrcConfig.getDevInfExts(); if (devInfExts != null) { // We must append this extra info devInf.addExts(devInfExts); } // A source can provide its own full device info, but for // backward compatibility this is not strictly necessary ds = syncMLSrcConfig.getDataStore(); } if (ds == null) { // Add one store for this source ds = new DataStore(); SourceRef sr = SourceRef.newInstance(); sr.setValue(sourceName); ds.setSourceRef(sr); CTInfo rxPref = new CTInfo(); rxPref.setCTType(sourceType); ds.setRxPref(rxPref); CTInfo txPref = new CTInfo(); txPref.setCTType(sourceType); ds.setTxPref(txPref); SyncCap syncCap = new SyncCap(); Vector types = new Vector(); types.addElement(SyncType.TWO_WAY); types.addElement(SyncType.SLOW); types.addElement(SyncType.SERVER_ALERTED); syncCap.setSyncType(types); ds.setSyncCap(syncCap); } Vector stores = new Vector(); stores.addElement(ds); devInf.setDataStores(stores); return devInf; } /** * Processes the modifications from the received response from server * * @param message The modification message from server * @param source the source to be synchronized * @return true if the incoming message contains a Final tag * @throws SyncException */ protected boolean processModifications(SyncML message, SyncSource source) throws SyncException { String msgId = null; if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "processModifications"); } SyncHdr hdr = message.getSyncHdr(); SyncBody body = message.getSyncBody(); if (hdr == null || body == null) { Log.error(TAG_LOG, "Invalid message from server."); throw new SyncException( SyncException.SERVER_ERROR, "Invalid message from server."); } globalNoResp = hasNoResp(hdr.getNoResp()); // Get the message id msgId = hdr.getMsgID(); // Ignore incoming modifications for one way from client modes (but // still process all status) boolean processIncomingMods = alertCode != SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT && alertCode != SyncML.ALERT_CODE_REFRESH_FROM_CLIENT && alertCode != SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT_NO_SLOW; itemsToProcess = new ItemsList(); statusToProcess = new Vector(); // Process all commands try { Vector commands = body.getCommands(); for(int j=0;j<commands.size();++j) { Object command = commands.elementAt(j); if (command instanceof Sync) { Sync sync = (Sync)command; processSyncCommand(sync, msgId); } else if (command instanceof Status) { Status status = (Status)command; processStatus(status); } else if (command instanceof SyncMLCommand) { if (processIncomingMods) { SyncMLCommand cmd = (SyncMLCommand)command; processCommand(cmd, msgId); } else { Log.error(TAG_LOG, "Ignoring server to client changes in one way sync"); } } else { Log.error(TAG_LOG, "Unknwon kind of command " + command); } } } finally { try { // The loop above collects all the item commands for the syn source // and we apply them in batch applySourceItems(msgId); // The loop above collects all the item commands for the syn source // and we apply them in batch applySourceStatus(); } finally { // Now we need to save the sync status because we changed it appliying modifications saveSyncStatus(); } } // Unless the server required no response, we shall send a Status to the // SyncHdr if (!globalNoResp) { Status hdrStatus = Status.newInstance(); hdrStatus.setMsgRef(""+msgID); hdrStatus.setCmdRef("0"); // FIXME hdrStatus.setCmd(SyncML.TAG_SYNCHDR); TargetRef tr = TargetRef.newInstance(); tr.setValue(deviceId); hdrStatus.setTargetRef(tr); SourceRef sr = SourceRef.newInstance(); sr.setValue(serverUrl); hdrStatus.setSourceRef(sr); hdrStatus.setData(Data.newInstance("" + SyncMLStatus.SUCCESS)); // Add this status to the list statusList.addElement(hdrStatus); } return body.isFinalMsg(); } protected void applySourceItems(String msgId) { // Now we can apply all the modification commands in one shot Vector syncItems = applySourceChanges(itemsToProcess); // Now we need to update the SyncStatus and create the proper // commands for(int i=0;i<itemsToProcess.size();++i) { Chunk chunk = (Chunk)itemsToProcess.elementAt(i); // It is possible that the sync source did not process all the items // In such a case we just ignore the skipped ones if (i == syncItems.size()) { break; } SyncItem item = (SyncItem)syncItems.elementAt(i); SyncMLCommand command = itemsToProcess.getItemCommand(chunk); String luid = item.getKey(); String guid = chunk.getKey(); int status = item.getSyncStatus(); if (SyncML.TAG_ADD.equals(command.getName())) { if (SyncMLStatus.isSuccess(status) && hierarchy != null) { hierarchy.put(guid, luid); } } syncStatus.addReceivedItem(guid, luid, command.getName(), status); // Generate the Status if required boolean noResp = globalNoResp || command.getNoResp(); Status statusCmd = null; if (noResp) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Found a command with NoResp (or SyncHdr NoResp), skipping status generation"); } } else { // Init the status object statusCmd = Status.newInstance(); statusCmd.setCmd(command.getName()); statusCmd.setCmdRef(command.getCmdId()); statusCmd.setMsgRef(msgId); // Save the source ref if present (ADD), otherwise the target ref (UPD & DEL) if (guid != null) { SourceRef sr = SourceRef.newInstance(); sr.setValue(guid); statusCmd.setSourceRef(sr); } else { TargetRef tr = TargetRef.newInstance(); tr.setValue(luid); statusCmd.setTargetRef(tr); } statusCmd.setData(Data.newInstance(""+status)); statusList.addElement(statusCmd); } } // Allow GC to pick memory itemsToProcess = null; } protected void applySourceStatus() { Vector sourceStatusList = new Vector(); for(int i=0;i<statusToProcess.size();++i) { Status status = (Status)statusToProcess.elementAt(i); String cmd = status.getCmd(); Vector items = status.getItems(); int code = status.getStatusCode(); if (code != SyncMLStatus.CHUNKED_ITEM_ACCEPTED) { // Check if it's a multi-item response if (items != null && items.size() > 0) { for (int j = 0, n = items.size(); j < n; j++) { Item item = (Item)items.elementAt(j); Target target = item.getTarget(); String key = null; if (target != null) { key = target.getLocURI(); } if (key == null) { Source source = item.getSource(); if (source != null) { key = source.getLocURI(); } } if (key != null) { if (SyncML.TAG_ADD.equals(cmd) || SyncML.TAG_REPLACE.equals(cmd) || SyncML.TAG_DELETE.equals(cmd)) { // The sync source is unware of chunks, it // is only interested at items if (code != SyncMLStatus.CHUNKED_ITEM_ACCEPTED) { sourceStatusList.addElement(new ItemStatus(key, getSourceStatusCode(code))); // Register the status for this item in // the current sync syncStatus.addSentItem(key, cmd); syncStatus.receivedItemStatus(key, code); } } else { sourceStatusList.addElement(new ItemStatus(key, getSourceStatusCode(code))); } } else { Log.error(TAG_LOG, "Cannot set item status for unknwon item"); } } } else { String ref = null; Vector srcRefs = status.getSourceRef(); if (srcRefs != null && srcRefs.size() > 0) { SourceRef srcRef = (SourceRef)srcRefs.elementAt(0); ref = srcRef.getValue(); } if (ref == null) { Vector tgtRefs = status.getTargetRef(); if (tgtRefs != null && tgtRefs.size() > 0) { TargetRef tgtRef = (TargetRef)tgtRefs.elementAt(0); ref = tgtRef.getValue(); } } if (ref == null) { Log.error(TAG_LOG, "Cannot set item status for unknown item"); } else { // The chunk accepted status (213) is not // propagated to the source, because the source // has no knowledge/visibility of the individual // chunks if (SyncML.TAG_ADD.equals(cmd) || SyncML.TAG_REPLACE.equals(cmd) || SyncML.TAG_DELETE.equals(cmd)) { if (code != SyncMLStatus.CHUNKED_ITEM_ACCEPTED) { sourceStatusList.addElement(new ItemStatus(ref, getSourceStatusCode(code))); // Register the status for this item in // the current sync syncStatus.addSentItem(ref, status.getCmd()); syncStatus.receivedItemStatus(ref, code); } } else { sourceStatusList.addElement(new ItemStatus(ref, getSourceStatusCode(code))); } } } } } // Apply all the statuses in one shot source.applyItemsStatus(sourceStatusList); // Allow the GC to pick this memory sourceStatusList = null; statusToProcess = null; } private void processStatus(Status status) throws SyncException { String cmd = status.getCmd(); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Processing Status for <" + cmd + "> command."); } // Check status to SyncHdr and Sync if (isSyncCommand(cmd)) { // In case of error we throw a SyncException if (!SyncMLStatus.isSuccess(status.getStatusCode())) { String msg = "Server responded " + status.getStatusCode() + " to command " + cmd; Log.error(TAG_LOG, msg); SyncException exc; switch(status.getStatusCode()) { case SyncMLStatus.SERVER_BUSY: // 503 exc = new SyncException(SyncException.SERVER_BUSY, msg); break; case SyncMLStatus.PROCESSING_ERROR: // 506 exc = new SyncException(SyncException.BACKEND_ERROR, msg); break; case SyncMLStatus.BACKEND_AUTH_ERROR: // 511 exc = new SyncException(SyncException.BACKEND_AUTH_ERROR, msg); break; default: // All error codes should be trapped by the above // cases, but to be conservative we leave this // fallback exc = new SyncException(SyncException.SERVER_ERROR, msg); break; } throw exc; } } else if (isMappingCommand(cmd)) { // The status of Map commands is ignored } else if (isPutCommand(cmd)) { // The status of Put commands is ignored } else if (isResultsCommand(cmd)) { // The status of Results commands is ignored } else if (isAlertCommand(cmd)) { // The status of Alert command is ignored } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Adding status to be processed"); } // Otherwise, pass it to the source statusToProcess.addElement(status); } } /** * Processes a modification command received from server, * returning the command parts in an Hashtable * * @param msgRef The messageId tag of the message containing this command * @param cmdName the command name * @param command the body of the command * * @return the number of modifications made * * @throws SyncException if the command parsing failed * */ protected int processCommand(SyncMLCommand command, String msgRef) throws SyncException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "processCommand"); } // Get the type of the items for this command, if present // otherwise use the type defined for this source. Meta meta = command.getMeta(); String itemType = null; if (meta != null) { itemType = meta.getType(); } // Set the command type or use the source one if (itemType != null) { command.setType(itemType); } else { command.setType(source.getType()); } String formatList[] = null; if (meta != null) { String format = meta.getFormat(); if (format != null) { formatList = StringUtil.split(format, ";"); } } Vector items = command.getItems(); for(int i=0;i<items.size();++i) { Item item = (Item)items.elementAt(i); Chunk chunk = createSyncItem(command, item, itemType, formatList); itemsToProcess.addElement(command, chunk); } return items.size(); } /** * Process the Sync command (check the source uri, save the * number of changes). * The visibility of this method is left at the package level so that we can * do unit tests. */ protected void processSyncCommand(Sync sync, String msgRef) throws SyncException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "processSyncCommand"); } String cmdId = sync.getCmdID(); Target target = sync.getTarget(); String locURI = null; if (target != null) { locURI = target.getLocURI(); } if (locURI == null || cmdId == null) { Log.error(TAG_LOG, "Invalid Sync command: "); throw new SyncException( SyncException.SERVER_ERROR, "Invalid Sync command from server."); } // If this sync is not for this source, throw an exception if (!locURI.equals(source.getName())) { Log.error(TAG_LOG, "Invalid uri: '" + locURI + "' for source: '" + source.getName() + "'"); throw new SyncException( SyncException.SERVER_ERROR, "Invalid source to sync: " + locURI); } Long nc = sync.getNumberOfChanges(); int ncVal = -1; if (nc != null) { ncVal = (int)nc.longValue(); } // This is the very first moment we know how many message we're about // to receive. This is when we notify the listener about it, even though // the receiving phase has already begun. getSyncListenerFromSource(source).startReceiving(ncVal); source.setServerItemsNumber(ncVal); Vector commands = sync.getCommands(); for(int i=0;i<commands.size();++i) { SyncMLCommand command = (SyncMLCommand)commands.elementAt(i); processCommand(command, msgRef); } boolean noResp = hasNoResp(sync.getNoResp()); // A NoResp in the Sync tag is considered global. This is not clear from // the spec (they state that a NoResp in the SyncHdr is to be considered // global) but some servers like Ovi expects this behavior. // TODO: shall this depend on the server? if (noResp && !globalNoResp) { globalNoResp = true; } if (noResp || globalNoResp) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Skipping status for sync command as NoResp was specified"); } } else { Status status = Status.newInstance(); status.setMsgRef(msgRef.toString()); status.setCmdRef(cmdId); status.setCmd(SyncML.TAG_SYNC); TargetRef tr = TargetRef.newInstance(); tr.setValue(source.getName()); status.setTargetRef(tr); SourceRef sr = SourceRef.newInstance(); sr.setValue(source.getSourceUri()); status.setSourceRef(sr); status.setData(Data.newInstance("" + SyncMLStatus.SUCCESS)); statusList.addElement(status); } } public Vector applySourceChanges(ItemsList items) throws SyncException { // The last item can be the beginning of a large object. LO are not sent to the source // until they are completed Chunk loChunk = null; SyncMLCommand loCmd = null; if (items.size() > 0) { Chunk lastItem = (Chunk)items.elementAt(items.size() - 1); if (lastItem.hasMoreData()) { loChunk = lastItem; loCmd = items.getItemCommand(loChunk); items.removeElementAt(items.size() - 1); } } // Apply all the other items. We need to create the SyncItem(s) SyncListener listener = getSyncListenerFromSource(source); Vector syncItems = sourceLOHandler.applyChanges(items, listener); if (loChunk != null) { // Apply the large object chunk via the source handler int status; char itemState = SyncML.TAG_ADD.equals(loCmd.getName()) ? SyncItem.STATE_NEW : SyncItem.STATE_UPDATED; status = sourceLOHandler.addUpdateChunk(loChunk, itemState == SyncItem.STATE_NEW); // Generate an item for this command SyncItem item = new SyncItem(loChunk.getKey(),loChunk.getType(), itemState,loChunk.getParent()); item.setSyncStatus(status); syncItems.addElement(item); } return syncItems; } protected Chunk createSyncItem(SyncMLCommand command, Item item, String itemType, String [] formatList) throws SyncException { int status = 0; String guid = null; String cmdTag = command.getName(); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "createSyncItem"); } Chunk chunk = sourceLOHandler.getItem(item, itemType, formatList, hierarchy); return chunk; } private boolean isSyncCommand(String cmd) { return cmd.equals(SyncML.TAG_SYNCHDR) || cmd.equals(SyncML.TAG_SYNC); } private boolean isMappingCommand(String cmd) { return cmd.equals(SyncML.TAG_MAP); } private boolean isPutCommand(String cmd) { return cmd.equals(SyncML.TAG_PUT); } private boolean isResultsCommand(String cmd) { return cmd.equals(SyncML.TAG_RESULTS); } private boolean isAlertCommand(String cmd) { return SyncML.TAG_ALERT.equals(cmd); } /** * Prepares the modification message in SyncML. * * @return the formatted message */ protected byte[] prepareModificationMessage() throws SyncException { ByteArrayOutputStream os = new ByteArrayOutputStream(); try { int msgSize = 0; SyncML msg = new SyncML(); // Prepare the header SyncHdr syncHdr = new SyncHdr(); // Prepare the VerDTD and VerProto tags VerDTD verDTD = new VerDTD("1.2"); syncHdr.setVerDTD(verDTD); syncHdr.setVerProto("SyncML/1.2"); // Set the session ID syncHdr.setSessionID(sessionID); // Set the message ID syncHdr.setMsgID(getNextMsgID()); // Set the source and the target Source hdrSource = Source.newInstance(); hdrSource.setLocURI(deviceId); syncHdr.setSource(hdrSource); Target hdrTarget = Target.newInstance(); hdrTarget.setLocURI(serverUrl); syncHdr.setTarget(hdrTarget); // Now create the sync header and add it to the msg msg.setSyncHdr(syncHdr); // Prepare the sync body SyncBody body = new SyncBody(); Vector bodyCommands = new Vector(); resetCmdID(); int msgIdRef = msgID - 1; // Update the estimated msgSize msgSize += wbxml ? SYNCML_WBXML_HDR_SIZE : SYNCML_XML_HDR_SIZE; // Add all the status commands... msgSize += prepareStatus(bodyCommands); // ...and cleanup the status vector statusList.removeAllElements(); // Add mappings if necessary. msgSize += prepareMappings(bodyCommands); //Adding the device capabilities as response to the <Get> command if (addDevInfResults != null) { Results devInfRes = new Results(); devInfRes.setCmdID(getNextCmdID()); devInfRes.setMsgRef(""+msgIdRef); devInfRes.setCmdRef(addDevInfResults); Meta devInfMeta = Meta.newInstance(); MetInf devInfMetInf = MetInf.newInstance(); // TODO at the moment the capabilities are always sent in xml, // even when we do WBXML sync if (wbxml) { devInfMetInf.setType("application/vnd.syncml-devinf+wbxml"); } else { devInfMetInf.setType("application/vnd.syncml-devinf+xml"); } devInfMeta.setMetInf(devInfMetInf); devInfRes.setMeta(devInfMeta); Item devInfItem = Item.newInstance(); Source src = Source.newInstance(); src.setLocURI(SyncML.DEVINF12); devInfItem.setSource(src); DevInf devInf = createDevInf(deviceConfig, source); Data data = Data.newInstance(devInf); devInfItem.setData(data); devInfRes.setItem(devInfItem); bodyCommands.addElement(devInfRes); //reset the flag addDevInfResults = null; } if (cancel && sendSuspendOnCancel) { // Prepare the suspend alert Alert suspendAlert = new Alert(); suspendAlert.setCmdID(getNextCmdID()); suspendAlert.setData(SyncML.ALERT_CODE_SUSPEND); Item alertItem = Item.newInstance(); Source alertSource = Source.newInstance(); alertSource.setLocURI(deviceId); alertItem.setSource(alertSource); Target alertTarget = Target.newInstance(); alertTarget.setLocURI(serverUrl); alertItem.setTarget(alertTarget); Vector alertItems = new Vector(1); alertItems.addElement(alertItem); suspendAlert.setItems(alertItems); bodyCommands.addElement(suspendAlert); suspendAlertSent = true; } else if (this.state == STATE_MODIFICATION_COMPLETED) { // We ask for the next message to the other side Alert nextMsgAlert = new Alert(); nextMsgAlert.setCmdID(getNextCmdID()); nextMsgAlert.setData(SyncML.ALERT_CODE_NEXT_MESSAGE); Item alertItem = Item.newInstance(); Source alertSource = Source.newInstance(); alertSource.setLocURI(source.getConfig().getName()); alertItem.setSource(alertSource); Target alertTarget = Target.newInstance(); alertTarget.setLocURI(source.getConfig().getRemoteUri()); alertItem.setTarget(alertTarget); Vector alertItems = new Vector(1); alertItems.addElement(alertItem); nextMsgAlert.setItems(alertItems); bodyCommands.addElement(nextMsgAlert); } // If we are cancelling then we shall not add the modifications to // this message if ((!cancel) || (!sendSuspendOnCancel)) { if (this.state != STATE_MODIFICATION_COMPLETED) { Sync syncCommand = prepareSyncTag(msgSize); bodyCommands.addElement(syncCommand); } if (this.state == STATE_MODIFICATION_COMPLETED) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Modification done, sending <final> tag."); } body.setFinalMsg(new Boolean(true)); } } body.setCommands(bodyCommands); msg.setSyncBody(body); logMessage(msg, false); formatter.format(msg, os, "UTF-8"); return os.toByteArray(); } catch (IOException ioe) { String msg = "Cannot prepare output message: " + ioe.toString(); Log.error(TAG_LOG, msg); throw new SyncException(SyncException.CLIENT_ERROR, msg); } } /** * return Sync tag about sourceUri * * @param size the current size of the msg being prepared * @return sync value */ private Sync prepareSyncTag(int size) throws SyncException { Sync syncCommand = new Sync(); syncCommand.setCmdID(getNextCmdID()); Target target = Target.newInstance(); target.setLocURI(source.getSourceUri()); syncCommand.setTarget(target); Source s = Source.newInstance(); s.setLocURI(source.getName()); syncCommand.setSource(s); Vector commands = new Vector(); do { int oldState = state; SyncMLCommand cmd = getNextCmd(size); // Last command? if (cmd == null) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "No more commands to send"); } break; } if (cmd.getCmdId() != null) { // Update the size size += cmd.getSize(); // append command commands.addElement(cmd); // If the message must be flushed here, we do it, but we restore the // previous state so that we can continue afterward if (state == STATE_FLUSHING_MSG) { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "SyncML msg flushed"); } nextState(oldState); break; } } } while (size < maxMsgSize); if (commands.size() > 0) { syncCommand.setCommands(commands); } return syncCommand; } private byte[] prepareMappingMessage() { return prepareMappingMessage(true); } protected int prepareStatus(Vector commands) { // Add the status of the other commands int l = statusList.size(); // Build status commands... for (int idx = 0; idx < l; idx++) { Status status = (Status) statusList.elementAt(idx); status.setCmdID(getNextCmdID()); commands.addElement(status); } // Return an estimated size for this set of commands int statusSize = wbxml ? SYNCML_WBXML_STATUS_SIZE : SYNCML_XML_STATUS_SIZE; return l * statusSize; } private byte[] prepareMappingMessage(boolean isAddStatusEnabled) { ByteArrayOutputStream os = new ByteArrayOutputStream(); try { SyncML msg = new SyncML(); // Prepare the header SyncHdr syncHdr = new SyncHdr(); // Prepare the VerDTD and VerProto tags VerDTD verDTD = new VerDTD("1.2"); syncHdr.setVerDTD(verDTD); syncHdr.setVerProto("SyncML/1.2"); // Set the session ID syncHdr.setSessionID(sessionID); // Set the message ID syncHdr.setMsgID(getNextMsgID()); // Set the source and the target Source hdrSource = Source.newInstance(); hdrSource.setLocURI(deviceId); syncHdr.setSource(hdrSource); Target hdrTarget = Target.newInstance(); hdrTarget.setLocURI(serverUrl); syncHdr.setTarget(hdrTarget); // Now create the sync header and add it to the msg msg.setSyncHdr(syncHdr); // Prepare the sync body SyncBody body = new SyncBody(); Vector bodyCommands = new Vector(); resetCmdID(); // Add all the status commands... prepareStatus(bodyCommands); // ...and cleanup the status vector statusList.removeAllElements(); // Add mappings if necessary. prepareMappings(bodyCommands); body.setFinalMsg(new Boolean(true)); body.setCommands(bodyCommands); msg.setSyncBody(body); logMessage(msg, false); formatter.format(msg, os, "UTF-8"); return os.toByteArray(); } catch (IOException ioe) { String msg = "Cannot prepare output message: " + ioe.toString(); Log.error(TAG_LOG, msg); throw new SyncException(SyncException.CLIENT_ERROR, msg); } } private int prepareMappings(Vector commands) throws IOException { Hashtable mappings = syncStatus.getPendingMappings(); if (mappings.size() > 0) { Map mapCommand = new Map(); mapCommand.setCmdID(getNextCmdID()); String sourceUri = source.getSourceUri(); String sourceName = source.getName(); Source source = Source.newInstance(); source.setLocURI(sourceName); mapCommand.setSource(source); Target target = Target.newInstance(); target.setLocURI(sourceUri); mapCommand.setTarget(target); Vector mapItems = new Vector(); Enumeration e = mappings.keys(); while (e.hasMoreElements()) { String sourceRef = (String) e.nextElement(); String targetRef = (String) mappings.get(sourceRef); MapItem mapItem = new MapItem(); target = Target.newInstance(); target.setLocURI(targetRef); mapItem.setTarget(target); source = Source.newInstance(); source.setLocURI(sourceRef); mapItem.setSource(source); mapItems.addElement(mapItem); } if (mapItems.size() > 0) { mapCommand.setMapItems(mapItems); } commands.addElement(mapCommand); } // Return an estimated size for this set of commands int mapSize = wbxml ? SYNCML_WBXML_MAP_SIZE : SYNCML_XML_MAP_SIZE; return mappings.size() * mapSize; } /** * This method returns the Add command tag. */ private SyncMLCommand getAddCommand(int size) throws SyncException { SyncMLCommand command = SyncMLCommand.newInstance(SyncML.TAG_ADD); int status = sourceLOHandler.getAddCommand(size, getSyncListenerFromSource(source), command, cmdID, syncStatus); if (status == SyncSourceLOHandler.DONE) { nextState(STATE_SENDING_REPLACE); } else if (status == SyncSourceLOHandler.FLUSH) { nextState(STATE_FLUSHING_MSG); } return command; } /** * This method returns the Replace command tag. */ private SyncMLCommand getReplaceCommand(int size) throws SyncException { SyncMLCommand command = SyncMLCommand.newInstance(SyncML.TAG_REPLACE); int status = sourceLOHandler.getReplaceCommand(size, getSyncListenerFromSource(source), command, cmdID); if (status == SyncSourceLOHandler.DONE) { nextState(STATE_SENDING_DELETE); } else if (status == SyncSourceLOHandler.FLUSH) { nextState(STATE_FLUSHING_MSG); } return command; } /** * This method returns the Delete command tag. */ private SyncMLCommand getDeleteCommand(int size) throws SyncException { SyncMLCommand command = SyncMLCommand.newInstance(SyncML.TAG_DELETE); boolean done = sourceLOHandler.getDeleteCommand(size, getSyncListenerFromSource(source), command, cmdID); // No item for this source if (done) { // All new items are donw, go to the next state. nextState(STATE_MODIFICATION_COMPLETED); } return command; } /** * Get the next command tag, with all the items that can be contained * in defined the message size. * * @param size * * @return the command tag of null if no item to send. */ private SyncMLCommand getNextCmd(int size) throws SyncException { SyncMLCommand command = null; switch (alertCode) { case SyncML.ALERT_CODE_SLOW: case SyncML.ALERT_CODE_REFRESH_FROM_CLIENT: int msgStatus[] = new int[1]; command = sourceLOHandler.getNextCommand(size, getSyncListenerFromSource(source), cmdID, syncStatus, msgStatus); int status = msgStatus[0]; if (status == SyncSourceLOHandler.DONE) { nextState(STATE_MODIFICATION_COMPLETED); } else if (status == SyncSourceLOHandler.FLUSH) { nextState(STATE_FLUSHING_MSG); } // Check if there are no items, then we signal the end of the // sync if (command.getCmdId() == null) { return null; } break; case SyncML.ALERT_CODE_REFRESH_FROM_SERVER: case SyncML.ALERT_CODE_ONE_WAY_FROM_SERVER: nextState(STATE_MODIFICATION_COMPLETED); return null; // no items sent for refresh from server case SyncML.ALERT_CODE_FAST: case SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT: case SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT_NO_SLOW: // // Fast Sync or One way from client. // switch (state) { case STATE_SENDING_ADD: command = getAddCommand(size); break; case STATE_SENDING_REPLACE: command = getReplaceCommand(size); break; case STATE_SENDING_DELETE: command = getDeleteCommand(size); break; default: return null; } break; default: Log.error(TAG_LOG, "Invalid alert code: " + alertCode); throw new SyncException( SyncException.SERVER_ERROR, "Invalid alert code: " + alertCode); } return command; } private void processMapResponse(SyncML message) throws SyncException { SyncBody body = message.getSyncBody(); // Process the body if (body == null) { return; } Vector commands = body.getCommands(); // Process all the commands and verify the status codes for the header // and the alert boolean hdrStatus = false; for(int i=0;i<commands.size();++i) { Object command = commands.elementAt(i); if (command instanceof Status) { Status status = (Status)command; String cmd = status.getCmd(); // At the moment we do not check for single map commands status, // we just check the status to the header command if (SyncML.TAG_SYNCHDR.equals(cmd)) { checkStatusCode(status); hdrStatus = true; } } } // If we did not receive the status(es) we expected, then we throw an // error if (!hdrStatus) { String msg = "Status code for map from server not received "; Log.error(TAG_LOG, msg); throw new SyncException(SyncException.SERVER_ERROR, msg); } } /** * Returns the server alert code for the given source * * @param sourceURI the source * * @return the server alert code for the given source or -1 if it is not * found/parsable */ private int getSourceAlertCode(String sourceURI) { try { String alert = (String) serverAlerts.get(sourceURI); return Integer.parseInt(alert); } catch (Throwable t) { Log.error(TAG_LOG, "ERROR: unrecognized server alert code (" + serverAlerts.get(sourceURI) + ") for " + sourceURI.toString(), t); } return -1; } // Reset the message ID counter. private void resetMsgID() { msgID = 0; } // Return the next message ID to use. private String getNextMsgID() { return String.valueOf(++msgID); } // Reset the command ID counter. private void resetCmdID() { cmdID.setValue(0); } // Return the next message ID to use. public String getNextCmdID() { return String.valueOf(cmdID.next()); } private void nextState(int state) { this.state = state; String msg = null; if (Log.getLogLevel() >= Log.DEBUG) { switch (state) { case STATE_SENDING_ADD: msg = "state=>STATE_SENDING_ADD"; break; case STATE_SENDING_REPLACE: msg = "state=>STATE_SENDING_REPLACE"; break; case STATE_SENDING_DELETE: msg = "state=>STATE_SENDING_DELETE"; break; case STATE_MODIFICATION_COMPLETED: msg = "state=>STATE_MODIFICATION_COMPLETED"; break; case STATE_FLUSHING_MSG: msg = "state=>STATE_FLUSHING_MSG"; break; default: msg = "UNKNOWN STATE!"; } if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, msg); } } } private void cancelSync() throws SyncException { if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, "Cancelling sync for source ["+source.getName()+"]"); } if (sendSuspendOnCancel) { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, "Need to send suspend alert"); } } else { throw new SyncException(SyncException.CANCELLED, "SyncManager sync got cancelled"); } } private boolean isSyncToBeCancelled() { return cancel; } private SyncListener getSyncListenerFromSource(SyncSource source) { SyncListener slistener = source.getListener(); if(slistener != null) { return slistener; } else { return basicListener; } } private int getListenerStatusFromSourceStatus(int status) { int syncStatus; switch(status) { case SyncSource.STATUS_SUCCESS: syncStatus = SyncListener.SUCCESS; break; case SyncSource.STATUS_SEND_ERROR: syncStatus = SyncListener.ERROR_SENDING_ITEMS; break; case SyncSource.STATUS_RECV_ERROR: syncStatus = SyncListener.ERROR_RECEIVING_ITEMS; break; case SyncSource.STATUS_SERVER_ERROR: case SyncSource.STATUS_CONNECTION_ERROR: default: syncStatus = SyncListener.GENERIC_ERROR; } return syncStatus; } private int getListenerStatusFromSyncException(SyncException se) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "getting listener status for " + se.getCode()); } int syncStatus; switch (se.getCode()) { case SyncException.AUTH_ERROR: syncStatus = SyncListener.INVALID_CREDENTIALS; break; case SyncException.FORBIDDEN_ERROR: syncStatus = SyncListener.FORBIDDEN_ERROR; break; case SyncException.CONN_NOT_FOUND: syncStatus = SyncListener.CONN_NOT_FOUND; break; case SyncException.READ_SERVER_RESPONSE_ERROR: syncStatus = SyncListener.READ_SERVER_RESPONSE_ERROR; break; case SyncException.WRITE_SERVER_REQUEST_ERROR: syncStatus = SyncListener.WRITE_SERVER_REQUEST_ERROR; break; case SyncException.SERVER_CONNECTION_REQUEST_ERROR: syncStatus = SyncListener.SERVER_CONNECTION_REQUEST_ERROR; break; case SyncException.BACKEND_AUTH_ERROR: syncStatus = SyncListener.BACKEND_AUTH_ERROR; break; case SyncException.NOT_FOUND_URI_ERROR: syncStatus = SyncListener.URI_NOT_FOUND_ERROR; break; case SyncException.CONNECTION_BLOCKED_BY_USER: syncStatus = SyncListener.CONNECTION_BLOCKED_BY_USER; break; case SyncException.SMART_SLOW_SYNC_UNSUPPORTED: syncStatus = SyncListener.SMART_SLOW_SYNC_UNSUPPORTED; break; case SyncException.CLIENT_ERROR: syncStatus = SyncListener.CLIENT_ERROR; break; case SyncException.ACCESS_ERROR: syncStatus = SyncListener.ACCESS_ERROR; break; case SyncException.DATA_NULL: syncStatus = SyncListener.DATA_NULL; break; case SyncException.ILLEGAL_ARGUMENT: syncStatus = SyncListener.ILLEGAL_ARGUMENT; break; case SyncException.SERVER_ERROR: syncStatus = SyncListener.SERVER_ERROR; break; case SyncException.SERVER_BUSY: syncStatus = SyncListener.SERVER_BUSY; break; case SyncException.BACKEND_ERROR: syncStatus = SyncListener.BACKEND_ERROR; break; case SyncException.CANCELLED: syncStatus = SyncListener.CANCELLED; break; case SyncException.NOT_SUPPORTED: syncStatus = SyncListener.NOT_SUPPORTED; break; case SyncException.ERR_READING_COMPRESSED_DATA: syncStatus = SyncListener.COMPRESSED_RESPONSE_ERROR; break; case SyncException.DEVICE_FULL: syncStatus = SyncListener.SERVER_FULL_ERROR; break; case SyncException.LOCAL_DEVICE_FULL: syncStatus = SyncListener.LOCAL_CLIENT_FULL_ERROR; break; default: syncStatus = SyncListener.GENERIC_ERROR; break; } return syncStatus; } private void releaseResources() { // Release resources this.syncStatus = null; this.hierarchy = null; this.statusList = null; this.source = null; this.sessionID = null; this.serverUrl = null; this.busy = false; this.devIntPutCmdID = null; ObjectsPool.releaseAll(); } private void logMessage(byte msg[], boolean hideData) { if (Log.getLogLevel() > Log.INFO) { try { // if the message is XML, there is no need to parse it here. // Just dump it if (wbxml) { SyncML syncMLMsg = parser.parse(msg); // We must format the message in XML and then print it SyncMLFormatter xmlFormatter = new SyncMLFormatter(false); xmlFormatter.setHideData(hideData); xmlFormatter.setPrettyPrint(true); ByteArrayOutputStream os = new ByteArrayOutputStream(); xmlFormatter.format(syncMLMsg, os, "UTF-8"); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, os.toString()); } } else { if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, new String(msg, "UTF-8")); } } } catch (Exception e) { Log.error(TAG_LOG, "Cannot print message: " + e.toString()); } } } private void logMessage(SyncML syncMLMsg, boolean hideData) { if (Log.getLogLevel() > Log.INFO) { try { // We must format the message in XML and then print it SyncMLFormatter xmlFormatter = new SyncMLFormatter(false); xmlFormatter.setHideData(hideData); xmlFormatter.setPrettyPrint(true); ByteArrayOutputStream os = new ByteArrayOutputStream(); xmlFormatter.format(syncMLMsg, os, "UTF-8"); if (Log.isLoggable(Log.DEBUG)) { Log.debug(TAG_LOG, os.toString()); } } catch (Exception e) { Log.error(TAG_LOG, "Cannot print message: " + e.toString()); } } } private void logBinaryMessage(byte msg[]) { if (logBinaryMessages && Log.getLogLevel() > Log.INFO) { StringBuffer binMsg = new StringBuffer(); for(int i=0;i<msg.length;++i) { byte b = msg[i]; int v = ((int)b) & 0xFF; String hexValue = Integer.toHexString(v); // The value must be printed as double digits if (hexValue.length() < 2) { hexValue = "0" + hexValue; } binMsg.append(hexValue); binMsg.append(" "); } if (Log.isLoggable(Log.INFO)) { Log.info(TAG_LOG, binMsg.toString()); } } } private boolean hasNoResp(Boolean nr) { if (nr != null && nr.booleanValue()) { return true; } else { return false; } } private void saveSyncStatus() { if (syncStatus != null) { try { syncStatus.save(); } catch (Exception e) { Log.error(TAG_LOG, "Cannot save sync status", e); } } } private int getSyncMLSyncMode(int sourceSyncMode) { int ret = SyncML.ALERT_CODE_FAST; switch (sourceSyncMode) { case SyncSource.FULL_SYNC: ret = SyncML.ALERT_CODE_SLOW; break; case SyncSource.FULL_UPLOAD: ret = SyncML.ALERT_CODE_REFRESH_FROM_CLIENT; break; case SyncSource.FULL_DOWNLOAD: ret = SyncML.ALERT_CODE_REFRESH_FROM_SERVER; break; case SyncSource.INCREMENTAL_SYNC: ret = SyncML.ALERT_CODE_FAST; break; case SyncSource.INCREMENTAL_UPLOAD: ret = SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT; break; case SyncSource.INCREMENTAL_DOWNLOAD: ret = SyncML.ALERT_CODE_ONE_WAY_FROM_SERVER; break; default: Log.error(TAG_LOG, "Unexpected source sync mode " + sourceSyncMode); } return ret; } private int getSourceSyncMode(int syncMLSyncMode) { int ret = SyncSource.INCREMENTAL_SYNC; switch (syncMLSyncMode) { case SyncML.ALERT_CODE_SLOW: ret = SyncSource.FULL_SYNC; break; case SyncML.ALERT_CODE_REFRESH_FROM_CLIENT: ret = SyncSource.FULL_UPLOAD; break; case SyncML.ALERT_CODE_REFRESH_FROM_SERVER: ret = SyncSource.FULL_DOWNLOAD; break; case SyncML.ALERT_CODE_FAST: ret = SyncSource.INCREMENTAL_SYNC; break; case SyncML.ALERT_CODE_ONE_WAY_FROM_CLIENT: ret = SyncSource.INCREMENTAL_UPLOAD; break; case SyncML.ALERT_CODE_ONE_WAY_FROM_SERVER: ret = SyncSource.INCREMENTAL_DOWNLOAD; break; default: Log.error(TAG_LOG, "Unexpected syncml sync mode " + syncMLSyncMode); } return ret; } private int getSourceStatusCode(int syncMLStatusCode) { if (SyncMLStatus.isSuccess(syncMLStatusCode)) { if (syncMLStatusCode == SyncMLStatus.CHUNKED_ITEM_ACCEPTED) { return SyncSource.CHUNK_SUCCESS_STATUS; } else { return SyncSource.SUCCESS_STATUS; } } else { if (syncMLStatusCode == SyncMLStatus.DEVICE_FULL) { return SyncSource.SERVER_FULL_ERROR_STATUS; } else { return SyncSource.ERROR_STATUS; } } } }