added support for partial Sync requests
authorLars Kneschke <l.kneschke@metaways.de>
Wed, 24 Oct 2012 05:25:53 +0000 (07:25 +0200)
committerLars Kneschke <l.kneschke@metaways.de>
Wed, 24 Oct 2012 05:25:53 +0000 (07:25 +0200)
Change-Id: Id5f3bf4083f20e941fb3de3f38c0e4376e5be83d

docs/syncroton.sql
lib/Syncroton/Command/Sync.php
lib/Syncroton/Model/IDevice.php
lib/Syncroton/Model/SyncCollection.php
tests/Syncroton/Command/SyncTests.php

index 133d70c..41d882b 100644 (file)
@@ -66,6 +66,7 @@ CREATE TABLE IF NOT EXISTS `syncroton_device` (
     `pinglifetime` int(11) DEFAULT NULL,
     `remotewipe` int(11) DEFAULT '0',
     `pingfolder` longblob,
+    `lastsynccollection` longblob DEFAULT NULL,
     'contactsfilter_id' varchar(40) DEFAULT NULL,
     'calendarfilter_id' varchar(40) DEFAULT NULL,
     'tasksfilter_id' varchar(40) DEFAULT NULL,
@@ -84,6 +85,7 @@ CREATE TABLE IF NOT EXISTS `syncroton_folder` (
     `type` int(11) NOT NULL,
     `creation_time` datetime NOT NULL,
     `lastfiltertype` int(11) DEFAULT NULL,
+    `supportedfields` longblob DEFAULT NULL,
     PRIMARY KEY (`id`),
     UNIQUE KEY `device_id--class--folderid` (`device_id`(40),`class`(40),`folderid`(40)),
     KEY `folderstates::device_id--devices::id` (`device_id`),
index bd26ff6..830e760 100644 (file)
@@ -128,252 +128,274 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
             $this->_globalWindowSize = $this->_maxWindowSize;
         }
         
-        // restore _collections from previous request
+        $collections = array();
+        
+        // try to restore collections from previous request
         if (isset($xml->Partial)) {
-            $this->_collections = array();
+            $decodedCollections = Zend_Json::decode($this->_device->lastsynccollection);
+            
+            if (is_array($decodedCollections)) {
+                foreach ($decodedCollections as $collection) {
+                    $collections[$collection['collectionId']] = new Syncroton_Model_SyncCollection($collection);
+                }
+            }
         }
         
-        // Collections element is option when Partial element is sent
+        // Collections element is optional when Partial element is sent
         if (isset($xml->Collections)) {
             foreach ($xml->Collections->Collection as $xmlCollection) {
-                $collectionData = new Syncroton_Model_SyncCollection($xmlCollection);
+                $collections[(string)$xmlCollection->CollectionId] = new Syncroton_Model_SyncCollection($xmlCollection);
+            }
+        }
                 
-                // got the folder synchronized to the device already
-                try {
-                    $collectionData->folder = $this->_folderBackend->getFolder($this->_device, $collectionData->collectionId);
-                    
-                } catch (Syncroton_Exception_NotFound $senf) {
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData->collectionId} not found");
-                    
-                    // trigger INVALID_SYNCKEY instead of OBJECT_NOTFOUND when synckey is higher than 0
-                    // to avoid a syncloop for the iPhone
-                    if ($collectionData->syncKey > 0) {
-                        $collectionData->folder    = new Syncroton_Model_Folder(array(
-                            'deviceId' => $this->_device,
-                            'serverId'  => $collectionData->collectionId
-                        ));
-                    }
-                    
-                    $this->_collections[$collectionData->collectionId] = $collectionData;
-                    
-                    continue;
+        foreach ($collections as $collectionData) {
+            // has the folder been synchronised to the device already
+            try {
+                $collectionData->folder = $this->_folderBackend->getFolder($this->_device, $collectionData->collectionId);
+                
+            } catch (Syncroton_Exception_NotFound $senf) {
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " folder {$collectionData->collectionId} not found");
+                
+                // trigger INVALID_SYNCKEY instead of OBJECT_NOTFOUND when synckey is higher than 0
+                // to avoid a syncloop for the iPhone
+                if ($collectionData->syncKey > 0) {
+                    $collectionData->folder    = new Syncroton_Model_Folder(array(
+                        'deviceId' => $this->_device,
+                        'serverId' => $collectionData->collectionId
+                    ));
                 }
                 
+                $this->_collections[$collectionData->collectionId] = $collectionData;
+                
+                continue;
+            }
+            
+            if ($this->_logger instanceof Zend_Log) 
+                $this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData->syncKey} Class: {$collectionData->folder->class} CollectionId: {$collectionData->collectionId}");
+            
+            // initial synckey
+            if($collectionData->syncKey === 0) {
                 if ($this->_logger instanceof Zend_Log) 
-                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData->syncKey} Class: {$collectionData->folder->class} CollectionId: {$collectionData->collectionId}");
+                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
                 
-                // initial synckey
-                if($collectionData->syncKey === 0) {
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " initial client synckey 0 provided");
+                // reset sync state for this folder
+                $this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
+                $this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
+            
+                $collectionData->syncState    = new Syncroton_Model_SyncState(array(
+                    'device_id' => $this->_device,
+                    'counter'   => 0,
+                    'type'      => $collectionData->folder,
+                    'lastsync'  => $this->_syncTimeStamp
+                ));
+                
+                $this->_collections[$collectionData->collectionId] = $collectionData;
+                
+                continue;
+            }
+            
+            // check for invalid sycnkey
+            if(($collectionData->syncState = $this->_syncStateBackend->validate($this->_device, $collectionData->folder, $collectionData->syncKey)) === false) {
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData->syncKey} provided");
+                
+                // reset sync state for this folder
+                $this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
+                $this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
+                
+                $this->_collections[$collectionData->collectionId] = $collectionData;
+                
+                continue;
+            }
+            
+            $dataController = Syncroton_Data_Factory::factory($collectionData->folder->class, $this->_device, $this->_syncTimeStamp);
+            
+            switch($collectionData->folder->class) {
+                case Syncroton_Data_Factory::CLASS_CALENDAR:
+                    $dataClass = 'Syncroton_Model_Event';
                     
-                    // reset sync state for this folder
-                    $this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
-                    $this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
-                
-                    $collectionData->syncState    = new Syncroton_Model_SyncState(array(
-                        'device_id' => $this->_device,
-                        'counter'   => 0,
-                        'type'      => $collectionData->folder,
-                        'lastsync'  => $this->_syncTimeStamp
-                    ));
+                    break;
                     
-                    $this->_collections[$collectionData->collectionId] = $collectionData;
+                case Syncroton_Data_Factory::CLASS_CONTACTS:
+                    $dataClass = 'Syncroton_Model_Contact';
                     
-                    continue;
-                }
-                
-                // check for invalid sycnkey
-                if(($collectionData->syncState = $this->_syncStateBackend->validate($this->_device, $collectionData->folder, $collectionData->syncKey)) === false) {
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " invalid synckey {$collectionData->syncKey} provided");
+                    break;
                     
-                    // reset sync state for this folder
-                    $this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
-                    $this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
+                case Syncroton_Data_Factory::CLASS_EMAIL:
+                    $dataClass = 'Syncroton_Model_Email';
                     
-                    $this->_collections[$collectionData->collectionId] = $collectionData;
+                    break;
                     
-                    continue;
-                }
-                
-                $dataController = Syncroton_Data_Factory::factory($collectionData->folder->class, $this->_device, $this->_syncTimeStamp);
-                
-                switch($collectionData->folder->class) {
-                    case Syncroton_Data_Factory::CLASS_CALENDAR:
-                        $dataClass = 'Syncroton_Model_Event';
-                        
-                        break;
-                        
-                    case Syncroton_Data_Factory::CLASS_CONTACTS:
-                        $dataClass = 'Syncroton_Model_Contact';
-                        
-                        break;
-                        
-                    case Syncroton_Data_Factory::CLASS_EMAIL:
-                        $dataClass = 'Syncroton_Model_Email';
-                        
-                        break;
-                        
-                    case Syncroton_Data_Factory::CLASS_TASKS:
-                        $dataClass = 'Syncroton_Model_Task';
-                        
-                        break;
-                        
-                    default:
-                        throw new Syncroton_Exception_UnexpectedValue('invalid class provided');
-                        
-                        break;
-                }
+                case Syncroton_Data_Factory::CLASS_TASKS:
+                    $dataClass = 'Syncroton_Model_Task';
+                    
+                    break;
+                    
+                default:
+                    throw new Syncroton_Exception_UnexpectedValue('invalid class provided');
+                    
+                    break;
+            }
+            
+            $clientModifications = array(
+                'added'            => array(),
+                'changed'          => array(),
+                'deleted'          => array(),
+                'forceAdd'         => array(),
+                'forceChange'      => array(),
+                'toBeFetched'      => array(),
+            );
+            
+            // handle incoming data
+            if($collectionData->hasClientAdds()) {
+                $adds = $collectionData->getClientAdds();
                 
-                $clientModifications = array(
-                    'added'            => array(),
-                    'changed'          => array(),
-                    'deleted'          => array(),
-                    'forceAdd'         => array(),
-                    'forceChange'      => array(),
-                    'toBeFetched'      => array(),
-                );
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
                 
-                // handle incoming data
-                if($collectionData->hasClientAdds()) {
-                    $adds = $collectionData->getClientAdds();
-                    
+                foreach ($adds as $add) {
                     if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($adds) . " entries to be added to server");
-                    
-                    foreach ($adds as $add) {
+                        $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
+
+                    try {
                         if ($this->_logger instanceof Zend_Log) 
-                            $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " add entry with clientId " . (string) $add->ClientId);
-    
-                        try {
-                            if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->info(__METHOD__ . '::' . __LINE__ . " adding entry as new");
-                            
-                            $serverId = $dataController->createEntry($collectionData->collectionId, new $dataClass($add->ApplicationData));
-                            
-                            $clientModifications['added'][$serverId] = array(
-                                'clientId'     => (string)$add->ClientId,
-                                'serverId'     => $serverId,
-                                'status'       => self::STATUS_SUCCESS,
-                                'contentState' => $this->_contentStateBackend->create(new Syncroton_Model_Content(array(
-                                    'device_id'        => $this->_device,
-                                    'folder_id'        => $collectionData->folder,
-                                    'contentid'        => $serverId,
-                                    'creation_time'    => $this->_syncTimeStamp,
-                                    'creation_synckey' => $collectionData->syncKey + 1
-                                )))
-                            );
-                            
-                        } catch (Exception $e) {
-                            if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
-                            $clientModifications['added'][] = array(
-                                'clientId' => (string)$add->ClientId,
-                                'status'   => self::STATUS_SERVER_ERROR
-                            );
-                        }
+                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . " adding entry as new");
+                        
+                        $serverId = $dataController->createEntry($collectionData->collectionId, new $dataClass($add->ApplicationData));
+                        
+                        $clientModifications['added'][$serverId] = array(
+                            'clientId'     => (string)$add->ClientId,
+                            'serverId'     => $serverId,
+                            'status'       => self::STATUS_SUCCESS,
+                            'contentState' => $this->_contentStateBackend->create(new Syncroton_Model_Content(array(
+                                'device_id'        => $this->_device,
+                                'folder_id'        => $collectionData->folder,
+                                'contentid'        => $serverId,
+                                'creation_time'    => $this->_syncTimeStamp,
+                                'creation_synckey' => $collectionData->syncKey + 1
+                            )))
+                        );
+                        
+                    } catch (Exception $e) {
+                        if ($this->_logger instanceof Zend_Log) 
+                            $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to add entry " . $e->getMessage());
+                        $clientModifications['added'][] = array(
+                            'clientId' => (string)$add->ClientId,
+                            'status'   => self::STATUS_SERVER_ERROR
+                        );
                     }
                 }
+            }
+            
+            // handle changes, but only if not first sync
+            if($collectionData->syncKey > 1 && $collectionData->hasClientChanges()) {
+                $changes = $collectionData->getClientChanges();
                 
-                // handle changes, but only if not first sync
-                if($collectionData->syncKey > 1 && $collectionData->hasClientChanges()) {
-                    $changes = $collectionData->getClientChanges();
-                    
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($changes) . " entries to be updated on server");
+                
+                foreach ($changes as $change) {
+                    $serverId = (string)$change->ServerId;
                     
-                    foreach ($changes as $change) {
-                        $serverId = (string)$change->ServerId;
+                    try {
+                        $dataController->updateEntry($collectionData->collectionId, $serverId, new $dataClass($change->ApplicationData));
+                        $clientModifications['changed'][$serverId] = self::STATUS_SUCCESS;
                         
-                        try {
-                            $dataController->updateEntry($collectionData->collectionId, $serverId, new $dataClass($change->ApplicationData));
-                            $clientModifications['changed'][$serverId] = self::STATUS_SUCCESS;
-                            
-                        } catch (Syncroton_Exception_AccessDenied $e) {
-                            $clientModifications['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
-                            $clientModifications['forceChange'][$serverId] = $serverId;
-                            
-                        } catch (Syncroton_Exception_NotFound $e) {
-                            // entry does not exist anymore, will get deleted automaticaly
-                            $clientModifications['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
-                            
-                        } catch (Exception $e) {
-                            if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
-                            // something went wrong while trying to update the entry
-                            $clientModifications['changed'][$serverId] = self::STATUS_SERVER_ERROR;
-                        }
+                    } catch (Syncroton_Exception_AccessDenied $e) {
+                        $clientModifications['changed'][$serverId] = self::STATUS_CONFLICT_MATCHING_THE_CLIENT_AND_SERVER_OBJECT;
+                        $clientModifications['forceChange'][$serverId] = $serverId;
+                        
+                    } catch (Syncroton_Exception_NotFound $e) {
+                        // entry does not exist anymore, will get deleted automaticaly
+                        $clientModifications['changed'][$serverId] = self::STATUS_OBJECT_NOT_FOUND;
+                        
+                    } catch (Exception $e) {
+                        if ($this->_logger instanceof Zend_Log) 
+                            $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " failed to update entry " . $e);
+                        // something went wrong while trying to update the entry
+                        $clientModifications['changed'][$serverId] = self::STATUS_SERVER_ERROR;
                     }
                 }
+            }
+            
+            // handle deletes, but only if not first sync
+            if($collectionData->hasClientDeletes()) {
+                $deletes = $collectionData->getClientDeletes();
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
                 
-                // handle deletes, but only if not first sync
-                if($collectionData->hasClientDeletes()) {
-                    $deletes = $collectionData->getClientDeletes();
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($deletes) . " entries to be deleted on server");
+                foreach ($deletes as $delete) {
+                    $serverId = (string)$delete->ServerId;
                     
-                    foreach ($deletes as $delete) {
-                        $serverId = (string)$delete->ServerId;
+                    try {
+                        // check if we have sent this entry to the phone
+                        $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
                         
                         try {
-                            // check if we have sent this entry to the phone
-                            $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData->folder, $serverId);
+                            $dataController->deleteEntry($collectionData->collectionId, $serverId, $collectionData);
                             
-                            try {
-                                $dataController->deleteEntry($collectionData->collectionId, $serverId, $collectionData);
-                                
-                            } catch(Syncroton_Exception_NotFound $e) {
-                                if ($this->_logger instanceof Zend_Log) 
-                                    $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
-                                
-                            } catch (Syncroton_Exception $e) {
-                                if ($this->_logger instanceof Zend_Log) 
-                                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
-                                $clientModifications['forceAdd'][$serverId] = $serverId;
-                            }
-                            $this->_contentStateBackend->delete($state);
+                        } catch(Syncroton_Exception_NotFound $e) {
+                            if ($this->_logger instanceof Zend_Log) 
+                                $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but entry was not found');
                             
-                        } catch (Syncroton_Exception_NotFound $senf) {
+                        } catch (Syncroton_Exception $e) {
                             if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
-                            // should we send a special status???
-                            //$collectionData->deleted[$serverId] = self::STATUS_SUCCESS;
+                                $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' tried to delete entry ' . $serverId . ' but a error occured: ' . $e->getMessage());
+                            $clientModifications['forceAdd'][$serverId] = $serverId;
                         }
+                        $this->_contentStateBackend->delete($state);
                         
-                        $clientModifications['deleted'][$serverId] = self::STATUS_SUCCESS;
+                    } catch (Syncroton_Exception_NotFound $senf) {
+                        if ($this->_logger instanceof Zend_Log) 
+                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . ' ' . $serverId . ' should have been removed from client already');
+                        // should we send a special status???
+                        //$collectionData->deleted[$serverId] = self::STATUS_SUCCESS;
                     }
+                    
+                    $clientModifications['deleted'][$serverId] = self::STATUS_SUCCESS;
                 }
+            }
+            
+            // handle fetches, but only if not first sync
+            if($collectionData->syncKey > 1 && $collectionData->hasClientFetches()) {
+                // the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
+                // some prevoius versions of iOS did not set GetChanges to 0 for fetches. Let's enforce getChanges to false here.
+                $collectionData->getChanges = false;
                 
-                // handle fetches, but only if not first sync
-                if($collectionData->syncKey > 1 && $collectionData->hasClientFetches()) {
-                    // the default value for GetChanges is 1. If the phone don't want the changes it must set GetChanges to 0
-                    // some prevoius versions of iOS did not set GetChanges to 0 for fetches. Let's enforce getChanges to false here.
-                    $collectionData->getChanges = false;
-                    
-                    $fetches = $collectionData->getClientFetches();
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
-                    
-                    $toBeFecthed = array();
-                    
-                    foreach ($fetches as $fetch) {
-                        $serverId = (string)$fetch->ServerId;
-                        
-                        $toBeFetched[$serverId] = $serverId;
-                    }
+                $fetches = $collectionData->getClientFetches();
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found " . count($fetches) . " entries to be fetched from server");
+                
+                $toBeFecthed = array();
+                
+                foreach ($fetches as $fetch) {
+                    $serverId = (string)$fetch->ServerId;
                     
-                    $collectionData->toBeFetched = $toBeFetched;
+                    $toBeFetched[$serverId] = $serverId;
                 }
                 
-                $this->_collections[$collectionData->collectionId] = $collectionData;
-                $this->_modifications[$collectionData->collectionId] = $clientModifications;
+                $collectionData->toBeFetched = $toBeFetched;
             }
+            
+            $this->_collections[$collectionData->collectionId] = $collectionData;
+            $this->_modifications[$collectionData->collectionId] = $clientModifications;
         }
         
         // store current value of $this->_collections for next Sync command request
-        
+        if (!isset($xml->Partial)) {
+            $collections = array();
+            
+            foreach ($this->_collections as $collection) {
+                if (isset($collection->syncState) && $collection->syncState->counter > 0 && !$collection->hasClientAdds() && !$collection->hasClientChanges()) { 
+                    $collections[$collection->collectionId] = $collection->toArray();
+                }
+            }
+            
+            $this->_device->lastsynccollection = Zend_Json::encode($collections);
+            
+            Syncroton_Registry::getDeviceBackend()->update($this->_device);
+        } 
     }
     
     /**
index b1177cb..e0c59ab 100644 (file)
@@ -36,6 +36,7 @@
  * @property    string   calendarfilter_id
  * @property    string   tasksfilter_id
  * @property    string   emailfilter_id
+ * @property    string   lastsynccollection
  */
 interface Syncroton_Model_IDevice
 {
index 29a151a..1199188 100644 (file)
  * @property    int     windowSize
  */
 
-class Syncroton_Model_SyncCollection
+class Syncroton_Model_SyncCollection extends Syncroton_Model_AEntry
 {
-    protected $_collection = array();
+    protected $_elements = array();
     
     protected $_xmlCollection;
     
+    protected $_xmlBaseElement = 'Collection';
+    
     public function __construct($properties = null)\r
     {\r
         if ($properties instanceof SimpleXMLElement) {\r
@@ -100,7 +102,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientAdds()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Add);
@@ -115,7 +117,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientChanges()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Change);
@@ -130,7 +132,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientDeletes()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Delete);
@@ -145,7 +147,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientFetches()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Fetch);
@@ -153,7 +155,7 @@ class Syncroton_Model_SyncCollection
     
     public function setFromArray(array $properties)\r
     {\r
-        $this->_collection = array('options' => array(
+        $this->_elements = array('options' => array(
             'filterType'      => Syncroton_Command_Sync::FILTER_NOTHING,
             'mimeSupport'     => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME,
             'mimeTruncation'  => Syncroton_Command_Sync::TRUNCATE_NOTHING,
@@ -172,25 +174,25 @@ class Syncroton_Model_SyncCollection
     
     /**
      * 
-     * @param SimpleXMLElement $xmlCollection
+     * @param SimpleXMLElement $properties
      * @throws InvalidArgumentException
      */
-    public function setFromSimpleXMLElement(SimpleXMLElement $xmlCollection)
+    public function setFromSimpleXMLElement(SimpleXMLElement $properties)
     {
-        if ($xmlCollection->getName() !== 'Collection') {
-            throw new InvalidArgumentException('Unexpected element name: ' . $xmlCollection->getName());
+        if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) {
+            throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
         }
         
-        $this->_xmlCollection = $xmlCollection;
+        $this->_xmlCollection = $properties;
         
-        $this->_collection = array(\r
-            'syncKey'          => (int)$xmlCollection->SyncKey,
-            'collectionId'     => (string)$xmlCollection->CollectionId,\r
-            'deletesAsMoves'   => isset($xmlCollection->DeletesAsMoves)   && (string)$xmlCollection->DeletesAsMoves   === '0' ? false : true,\r
-            'conversationMode' => isset($xmlCollection->ConversationMode) && (string)$xmlCollection->ConversationMode === '0' ? false : true,\r
-            'getChanges'       => isset($xmlCollection->GetChanges)       && (string) $xmlCollection->GetChanges      === '0' ? false : true,
-            'windowSize'       => isset($xmlCollection->WindowSize) ? (int)$xmlCollection->WindowSize : 100,
-            'class'            => isset($xmlCollection->Class)      ? (string)$xmlCollection->Class   : null,
+        $this->_elements = array(\r
+            'syncKey'          => (int)$properties->SyncKey,
+            'collectionId'     => (string)$properties->CollectionId,\r
+            'deletesAsMoves'   => isset($properties->DeletesAsMoves)   && (string)$properties->DeletesAsMoves   === '0' ? false : true,\r
+            'conversationMode' => isset($properties->ConversationMode) && (string)$properties->ConversationMode === '0' ? false : true,\r
+            'getChanges'       => isset($properties->GetChanges)       && (string) $properties->GetChanges      === '0' ? false : true,
+            'windowSize'       => isset($properties->WindowSize) ? (int)$properties->WindowSize : 100,
+            'class'            => isset($properties->Class)      ? (string)$properties->Class   : null,
             'options'          => array(
                 'filterType'      => Syncroton_Command_Sync::FILTER_NOTHING,
                 'mimeSupport'     => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME,
@@ -202,40 +204,40 @@ class Syncroton_Model_SyncCollection
             'folder'           => null\r
         );
         
-        if (isset($xmlCollection->Supported)) {
+        if (isset($properties->Supported)) {
             // @todo collected supported elements
         }
         \r
         // process options\r
-        if (isset($xmlCollection->Options)) {\r
+        if (isset($properties->Options)) {\r
             // optional parameters\r
-            if (isset($xmlCollection->Options->FilterType)) {\r
-                $this->_collection['options']['filterType'] = (int)$xmlCollection->Options->FilterType;\r
+            if (isset($properties->Options->FilterType)) {\r
+                $this->_elements['options']['filterType'] = (int)$properties->Options->FilterType;\r
             }\r
-            if (isset($xmlCollection->Options->MIMESupport)) {\r
-                $this->_collection['options']['mimeSupport'] = (int)$xmlCollection->Options->MIMESupport;\r
+            if (isset($properties->Options->MIMESupport)) {\r
+                $this->_elements['options']['mimeSupport'] = (int)$properties->Options->MIMESupport;\r
             }\r
-            if (isset($xmlCollection->Options->MIMETruncation)) {\r
-                $this->_collection['options']['mimeTruncation'] = (int)$xmlCollection->Options->MIMETruncation;\r
+            if (isset($properties->Options->MIMETruncation)) {\r
+                $this->_elements['options']['mimeTruncation'] = (int)$properties->Options->MIMETruncation;\r
             }
-            if (isset($xmlCollection->Options->Class)) {\r
-                $this->_collection['options']['class'] = (string)$xmlCollection->Options->Class;\r
+            if (isset($properties->Options->Class)) {\r
+                $this->_elements['options']['class'] = (string)$properties->Options->Class;\r
             }\r
             \r
             // try to fetch element from AirSyncBase:BodyPreference\r
-            $airSyncBase = $xmlCollection->Options->children('uri:AirSyncBase');\r
+            $airSyncBase = $properties->Options->children('uri:AirSyncBase');\r
             \r
             if (isset($airSyncBase->BodyPreference)) {\r
                 \r
                 foreach ($airSyncBase->BodyPreference as $bodyPreference) {\r
                     $type = (int) $bodyPreference->Type;\r
-                    $this->_collection['options']['bodyPreferences'][$type] = array(\r
+                    $this->_elements['options']['bodyPreferences'][$type] = array(\r
                         'type' => $type\r
                     );\r
                     \r
                     // optional\r
                     if (isset($bodyPreference->TruncationSize)) {\r
-                        $this->_collection['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;\r
+                        $this->_elements['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;\r
                     }\r
                 }
             }
@@ -246,10 +248,23 @@ class Syncroton_Model_SyncCollection
         }\r
     }
     
+    public function toArray()
+    {
+        $result = array();
+        
+        foreach (array('syncKey', 'collectionId', 'deletesAsMoves', 'conversationMode', 'getChanges', 'windowSize', 'class', 'options') as $key) {
+            if (isset($this->$key)) {
+                $result[$key] = $this->$key;
+            }
+        }
+        
+        return $result;
+    }
+    
     public function &__get($name)
     {
-        if (array_key_exists($name, $this->_collection)) {\r
-            return $this->_collection[$name];\r
+        if (array_key_exists($name, $this->_elements)) {\r
+            return $this->_elements[$name];\r
         }
         //echo $name . PHP_EOL;
         return null; 
@@ -257,16 +272,6 @@ class Syncroton_Model_SyncCollection
     
     public function __set($name, $value)
     {
-        $this->_collection[$name] = $value;
-    }
-    
-    public function __isset($name)\r
-    {\r
-        return isset($this->_collection[$name]);\r
-    }\r
-    \r
-    public function __unset($name)\r
-    {\r
-        unset($this->_collection[$name]);\r
+        $this->_elements[$name] = $value;
     }
 }
\ No newline at end of file
index 27bc4a5..8de0391 100644 (file)
@@ -783,6 +783,42 @@ class Syncroton_Command_SyncTests extends Syncroton_Command_ATestCase
     }
     
     /**
+     * test sync with Partial element
+     * 
+     * the SyncKey should not change
+     */
+    public function testSyncWithPartialElement()
+    {
+        $serverId = $this->testSyncWithNoChanges();
+        
+        $doc = new DOMDocument();
+        $doc->loadXML('<?xml version="1.0" encoding="utf-8"?>
+            <!DOCTYPE AirSync PUBLIC "-//AIRSYNC//DTD AirSync//EN" "http://www.microsoft.com/">
+            <Sync xmlns="uri:AirSync" xmlns:AirSyncBase="uri:AirSyncBase">
+                <Partial/>
+            </Sync>'
+        );
+        
+        $sync = new Syncroton_Command_Sync($doc, $this->_device, $this->_device->policykey);
+        
+        $sync->handle();
+        
+        $syncDoc = $sync->getResponse();
+        #$syncDoc->formatOutput = true; echo $syncDoc->saveXML();
+
+        $xpath = new DomXPath($syncDoc);
+        $xpath->registerNamespace('AirSync', 'uri:AirSync');
+        
+        $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:SyncKey');
+        $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
+        $this->assertEquals(4, $nodes->item(0)->nodeValue, $syncDoc->saveXML());
+        
+        $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:Status');
+        $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
+        $this->assertEquals(Syncroton_Command_Sync::STATUS_SUCCESS, $nodes->item(0)->nodeValue, $syncDoc->saveXML());
+    }
+    
+    /**
      * @return string the id of the newly created contact
      */
     public function testConcurringSyncRequest()