wrap Sync and FolderSync command processing into one transaction
authorLars Kneschke <l.kneschke@metaways.de>
Fri, 17 Feb 2012 09:19:45 +0000 (10:19 +0100)
committerLars Kneschke <l.kneschke@metaways.de>
Fri, 17 Feb 2012 09:19:45 +0000 (10:19 +0100)
lib/Syncope/Command/FolderSync.php
lib/Syncope/Command/Sync.php
lib/Syncope/Command/Wbxml.php
lib/Syncope/Server.php
tests/Syncope/Command/SyncTests.php

index 8c7196d..2ab2138 100644 (file)
@@ -73,6 +73,9 @@ class Syncope_Command_FolderSync extends Syncope_Command_Wbxml
      */
     public function handle()
     {
+        // wrap whole FolderSync command in one transaction
+        $this->_transactionId = Syncope_Registry::getTransactionManager()->startTransaction(Syncope_Registry::getDatabase());
+        
         $xml = simplexml_import_dom($this->_inputDom);
         $syncKey = (int)$xml->SyncKey;
         
@@ -211,6 +214,8 @@ class Syncope_Command_FolderSync extends Syncope_Command_Wbxml
             }
         }
         
+        Syncope_Registry::getTransactionManager()->commitTransaction($this->_transactionId);
+        
         return $this->_outputDom;
     }    
 }
index 31254f5..5094f22 100644 (file)
@@ -106,6 +106,9 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
      */
     public function handle()
     {
+        // wrap whole Sync command in one transaction
+        $this->_transactionId = Syncope_Registry::getTransactionManager()->startTransaction(Syncope_Registry::getDatabase());
+        
         // input xml
         $xml = simplexml_import_dom($this->_inputDom);
         
@@ -161,6 +164,9 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
                 }
             }
             
+            $collectionData['syncState'] = null;
+            $collectionData['folder']    = null;
+            
             // got the folder synchronized to the device already
             try {
                 $collectionData['folder'] = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
@@ -172,16 +178,18 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
                 // trigger INVALID_SYNCKEY instead of OBJECT_NOTFOUND when synckey is bigger than 0
                 // to avoid a syncloop for the iPhone
                 if ($collectionData['syncKey'] > 0) {
-                    $collectionData['folder']    = 'foobar';
-                    $collectionData['syncState'] = false;
-                    
+                    $collectionData['folder']    = new Syncope_Model_Folder(array(
+                        'device_id' => $this->_device,
+                        'folderid'  => $collectionData['collectionId']
+                    ));
                 }
-                $this->_collections['invalidFolderId'][$collectionData['collectionId']] = $collectionData;
+                
+                $this->_collections[] = $collectionData;
                 continue;
             }
             
             if ($this->_logger instanceof Zend_Log) 
-                $this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData['syncKey']} Class: {$collectionData['class']} CollectionId: {$collectionData['collectionId']}");
+                $this->_logger->info(__METHOD__ . '::' . __LINE__ . " SyncKey is {$collectionData['syncKey']} Class: {$collectionData['folder']->class} CollectionId: {$collectionData['collectionId']}");
             
             // initial synckey
             if($collectionData['syncKey'] === 0) {
@@ -199,7 +207,7 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
                     'lastsync'  => $this->_syncTimeStamp
                 ));
                 
-                $this->_collections[$collectionData['folder']->class][$collectionData['collectionId']] = $collectionData;
+                $this->_collections[] = $collectionData;
                 
                 continue;
             }
@@ -213,7 +221,7 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
                 $this->_syncStateBackend->resetState($this->_device, $collectionData['folder']);
                 $this->_contentStateBackend->resetState($this->_device, $collectionData['folder']);
                 
-                $this->_collections[$collectionData['folder']->class][$collectionData['collectionId']] = $collectionData;
+                $this->_collections[] = $collectionData;
                 
                 continue;
             }
@@ -342,7 +350,7 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
                 }
             }
             
-            $this->_collections[$collectionData['folder']->class][$collectionData['collectionId']] = $collectionData;
+            $this->_collections[] = $collectionData;
         }
     }
     
@@ -358,374 +366,367 @@ class Syncope_Command_Sync extends Syncope_Command_Wbxml
         
         $collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections'));
 
-        foreach($this->_collections as $classCollections) {
-            foreach($classCollections as $collectionId => $collectionData) {
-                
-                // invalid collectionid provided
-                if (empty($collectionData['folder'])) {
-                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
-                    if (!empty($collectionData['class'])) {
-                        $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
-                    }
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
-                    /**
-                     * i would expect to send STATUS_FOLDER_HIERARCHY_HAS_CHANGED but by reading the source code of Android I found out
-                     * that android triggers the FolderSync only on STATUS_OBJECT_NOT_FOUND
-                     */
-                    #$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
+        foreach($this->_collections as $collectionData) {
+            // invalid collectionid provided
+            if (! ($collectionData['folder'] instanceof Syncope_Model_IFolder)) {
+                $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                /**
+                 * i would expect to send STATUS_FOLDER_HIERARCHY_HAS_CHANGED but by reading the source code of Android I found out
+                 * that android triggers the FolderSync only on STATUS_OBJECT_NOT_FOUND
+                 */
+                #$collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_FOLDER_HIERARCHY_HAS_CHANGED));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
 
-                // invalid synckey provided
-                } elseif (! ($collectionData['syncState'] instanceof Syncope_Model_ISyncState)) {
-                    // set synckey to 0
-                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
-                    if (!empty($collectionData['class'])) {
-                        $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
-                    }
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
-                    
+            // invalid synckey provided
+            } elseif (! ($collectionData['syncState'] instanceof Syncope_Model_ISyncState)) {
+                // set synckey to 0
+                $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', 0));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_INVALID_SYNC_KEY));
+                
+
+            // initial sync
+            } elseif ($collectionData['syncState']->counter === 0) {
+                $collectionData['syncState']->counter++;
 
                 // initial sync
-                } elseif ($collectionData['syncState']->counter === 0) {
-                    $collectionData['syncState']->counter++;
-    
-                    // initial sync
-                    // send back a new SyncKey only
-                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
-                    if (!empty($collectionData['class'])) {
-                        $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
-                    }
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
-                    
-                } else {
+                // send back a new SyncKey only
+                $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                if (!empty($collectionData['folder']->class)) {
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['folder']->class));
+                }
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
+                
+            } else {
 
-                    $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class , $this->_device, $this->_syncTimeStamp);
-                    
-                    $serverAdds    = array();
-                    $serverChanges = array();
-                    $serverDeletes = array();
-                    $moreAvailable = false;
-                    
-                    if($collectionData['getChanges'] === true) {
-                        if($collectionData['syncKey'] === 1) {
-                            // get all available entries
-                            $serverAdds    = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
+                $dataController = Syncope_Data_Factory::factory($collectionData['folder']->class , $this->_device, $this->_syncTimeStamp);
+                
+                $serverAdds    = array();
+                $serverChanges = array();
+                $serverDeletes = array();
+                $moreAvailable = false;
+                
+                if($collectionData['getChanges'] === true) {
+                    if($collectionData['syncKey'] === 1) {
+                        // get all available entries
+                        $serverAdds    = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
+                        
+                    } else {
+                        // continue sync session?
+                        if(is_array($collectionData['syncState']->pendingdata)) {
+                            if ($this->_logger instanceof Zend_Log)
+                                $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
                             
+                            $serverAdds    = $collectionData['syncState']->pendingdata['serverAdds'];
+                            $serverChanges = $collectionData['syncState']->pendingdata['serverChanges'];
+                            $serverDeletes = $collectionData['syncState']->pendingdata['serverDeletes'];
                         } else {
-                            // continue sync session?
-                            if(is_array($collectionData['syncState']->pendingdata)) {
-                                if ($this->_logger instanceof Zend_Log)
-                                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " restored from sync state ");
-                                
-                                $serverAdds    = $collectionData['syncState']->pendingdata['serverAdds'];
-                                $serverChanges = $collectionData['syncState']->pendingdata['serverChanges'];
-                                $serverDeletes = $collectionData['syncState']->pendingdata['serverDeletes'];
-                            } else {
-                                // fetch entries added since last sync
-                                $allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData['folder']);
-                                $allServerEntries = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
-                    
-                                // add entries
-                                $serverDiff = array_diff($allServerEntries, $allClientEntries);
-                                // add entries which produced problems during delete from client
-                                $serverAdds = $collectionData['forceAdd'];
-                                // add entries not yet sent to client
-                                $serverAdds = array_unique(array_merge($serverAdds, $serverDiff));
-                    
-                                # @todo still needed?
-                                foreach($serverAdds as $id => $serverId) {
-                                    // skip entries added by client during this sync session
-                                    if(isset($collectionData['added'][$serverId]) && !isset($collectionData['forceAdd'][$serverId])) {
-                                        if ($this->_logger instanceof Zend_Log)
-                                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
-                                        unset($serverAdds[$id]);
-                                    }
-                                }
-                    
-                                // entries to be deleted
-                                $serverDeletes = array_diff($allClientEntries, $allServerEntries);
-                    
-                                // fetch entries changed since last sync
-                                $serverChanges = $dataController->getChangedEntries($collectionData['collectionId'], $collectionData['syncState']->lastsync, $this->_syncTimeStamp);
-                                $serverChanges = array_merge($serverChanges, $collectionData['forceChange']);
-                    
-                                foreach($serverChanges as $id => $serverId) {
-                                    // skip entry, if it got changed by client during current sync
-                                    if(isset($collectionData['changed'][$serverId]) && !isset($collectionData['forceChange'][$serverId])) {
-                                        if ($this->_logger instanceof Zend_Log)
-                                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
-                                        unset($serverChanges[$id]);
-                                    }
+                            // fetch entries added since last sync
+                            $allClientEntries = $this->_contentStateBackend->getFolderState($this->_device, $collectionData['folder']);
+                            $allServerEntries = $dataController->getServerEntries($collectionData['collectionId'], $collectionData['filterType']);
+                
+                            // add entries
+                            $serverDiff = array_diff($allServerEntries, $allClientEntries);
+                            // add entries which produced problems during delete from client
+                            $serverAdds = $collectionData['forceAdd'];
+                            // add entries not yet sent to client
+                            $serverAdds = array_unique(array_merge($serverAdds, $serverDiff));
+                
+                            # @todo still needed?
+                            foreach($serverAdds as $id => $serverId) {
+                                // skip entries added by client during this sync session
+                                if(isset($collectionData['added'][$serverId]) && !isset($collectionData['forceAdd'][$serverId])) {
+                                    if ($this->_logger instanceof Zend_Log)
+                                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped added entry: " . $serverId);
+                                    unset($serverAdds[$id]);
                                 }
-                    
-                                // entries comeing in scope are already in $serverAdds and do not need to
-                                // be send with $serverCanges
-                                $serverChanges = array_diff($serverChanges, $serverAdds);
                             }
-                        }
-                    
-                        if ($this->_logger instanceof Zend_Log)
-                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverAdds) . '/' . count($serverChanges) . '/' . count($serverDeletes)  . ' entries for sync from server to client');
-                    }
-                    
-                    
-                    
-                    
-                    if (!empty($collectionData['added']) || !empty($collectionData['changed']) || !empty($collectionData['deleted']) ||
-                        !empty($serverAdds) || !empty($serverChanges) || !empty($serverDeletes)) {
-                        $collectionData['syncState']->counter++;
-                    }
-                    
-                    // collection header
-                    $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
-                    if (!empty($collectionData['class'])) {
-                        $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['class']));
-                    }
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
-                    
-                    $responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
-                    
-                    // send reponse for newly added entries
-                    if(!empty($collectionData['added'])) {
-                        foreach($collectionData['added'] as $entryData) {
-                            $add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
-                            $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
-                            // we have no serverId is the add failed
-                            if(isset($entryData['serverId'])) {
-                                $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
+                
+                            // entries to be deleted
+                            $serverDeletes = array_diff($allClientEntries, $allServerEntries);
+                
+                            // fetch entries changed since last sync
+                            $serverChanges = $dataController->getChangedEntries($collectionData['collectionId'], $collectionData['syncState']->lastsync, $this->_syncTimeStamp);
+                            $serverChanges = array_merge($serverChanges, $collectionData['forceChange']);
+                
+                            foreach($serverChanges as $id => $serverId) {
+                                // skip entry, if it got changed by client during current sync
+                                if(isset($collectionData['changed'][$serverId]) && !isset($collectionData['forceChange'][$serverId])) {
+                                    if ($this->_logger instanceof Zend_Log)
+                                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped changed entry: " . $serverId);
+                                    unset($serverChanges[$id]);
+                                }
                             }
-                            $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
+                
+                            // entries comeing in scope are already in $serverAdds and do not need to
+                            // be send with $serverCanges
+                            $serverChanges = array_diff($serverChanges, $serverAdds);
                         }
                     }
-                    
-                    // send reponse for changed entries
-                    if(!empty($collectionData['changed'])) {
-                        foreach($collectionData['changed'] as $serverId => $status) {
-                            if ($status !== Syncope_Command_Sync::STATUS_SUCCESS) {
-                                $change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
-                                $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
-                                $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
-                            }
+                
+                    if ($this->_logger instanceof Zend_Log)
+                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " found (added/changed/deleted) " . count($serverAdds) . '/' . count($serverChanges) . '/' . count($serverDeletes)  . ' entries for sync from server to client');
+                }
+                
+                
+                
+                
+                if (!empty($collectionData['added']) || !empty($collectionData['changed']) || !empty($collectionData['deleted']) ||
+                    !empty($serverAdds) || !empty($serverChanges) || !empty($serverDeletes)) {
+                    $collectionData['syncState']->counter++;
+                }
+                
+                // collection header
+                $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
+                if (!empty($collectionData['folder']->class)) {
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData['folder']->class));
+                }
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey', $collectionData['syncState']->counter));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData['collectionId']));
+                $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
+                
+                $responses = $this->_outputDom->createElementNS('uri:AirSync', 'Responses');
+                
+                // send reponse for newly added entries
+                if(!empty($collectionData['added'])) {
+                    foreach($collectionData['added'] as $entryData) {
+                        $add = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Add'));
+                        $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ClientId', $entryData['clientId']));
+                        // we have no serverId is the add failed
+                        if(isset($entryData['serverId'])) {
+                            $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $entryData['serverId']));
                         }
+                        $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $entryData['status']));
                     }
-                    
-                    // send response for to be fetched entries
-                    if(!empty($collectionData['toBeFetched'])) {
-                        foreach($collectionData['toBeFetched'] as $serverId) {
-                            $fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
-                            $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
-                            
-                            try {
-                                $applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
-                                $dataController->appendXML($applicationData, $collectionData, $serverId);
-                                
-                                $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
-                                
-                                $fetch->appendChild($applicationData);
-                            } catch (Exception $e) {
-                                if ($this->_logger instanceof Zend_Log) 
-                                    $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
-                                $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
-                            }
+                }
+                
+                // send reponse for changed entries
+                if(!empty($collectionData['changed'])) {
+                    foreach($collectionData['changed'] as $serverId => $status) {
+                        if ($status !== Syncope_Command_Sync::STATUS_SUCCESS) {
+                            $change = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Change'));
+                            $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                            $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', $status));
                         }
                     }
-                    
-                    if ($responses->hasChildNodes() === true) {
-                        $collection->appendChild($responses);
-                    }
-                    
-                    if ((count($serverAdds) + count($serverChanges) + count($serverDeletes)) > $collectionData['windowSize'] ) {
-                        $moreAvailable = true;
-                        $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
-                    }
-                    
-                    $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
-                    
-                    
-                    /**
-                     * process entries added on server side
-                     */
-                    $newContentStates = array();
-                    
-                    foreach($serverAdds as $id => $serverId) {
-                        if($this->_totalCount === $collectionData['windowSize']) {
-                            break;
-                        }
-                        
-                        /**
-                         * somewhere is a problem in the logic for handling moreAvailable
-                         * 
-                         * it can happen, that we have a contentstate (which means we sent the entry to the client
-                         * and that this entry is yet in $collectionData['syncState']->pendingdata['serverAdds']
-                         * I have no idea how this can happen, but the next lines of code work around this problem
-                         */
-                        try {
-                            $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
-                        
-                            if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client");
-                            
-                            unset($serverAdds[$id]);
-                            continue;
-                            
-                        } catch (Syncope_Exception_NotFound $senf) {
-                            // do nothing => content state should not exist yet
-                        }
+                }
+                
+                // send response for to be fetched entries
+                if(!empty($collectionData['toBeFetched'])) {
+                    foreach($collectionData['toBeFetched'] as $serverId) {
+                        $fetch = $responses->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Fetch'));
+                        $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                         
                         try {
-                            $add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
-                            $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
-                            
-                            $applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
+                            $applicationData = $this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData');
                             $dataController->appendXML($applicationData, $collectionData, $serverId);
-    
-                            $commands->appendChild($add);
-                            
-                            $this->_totalCount++;
-                        } catch (Exception $e) {
-                            if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
-                        }
-                        
-                        // mark as send to the client, even the conversion to xml might have failed 
-                        $newContentStates[] = new Syncope_Model_Content(array(
-                            'device_id'     => $this->_device,
-                            'folder_id'     => $collectionData['folder'],
-                            'contentid'     => $serverId,
-                            'creation_time' => $this->_syncTimeStamp
-                        ));
-                        unset($serverAdds[$id]);    
-                    }
-
-                    /**
-                     * process entries changed on server side
-                     */
-                    foreach($serverChanges as $id => $serverId) {
-                        if($this->_totalCount === $collectionData['windowSize']) {
-                            break;
-                        }
-
-                        try {
-                            $change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
-                            $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
                             
-                            $applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
-                            $dataController->appendXML($applicationData, $collectionData, $serverId);
-    
-                            $commands->appendChild($change);
+                            $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
                             
-                            $this->_totalCount++;
+                            $fetch->appendChild($applicationData);
                         } catch (Exception $e) {
                             if ($this->_logger instanceof Zend_Log) 
                                 $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
+                            $fetch->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_OBJECT_NOT_FOUND));
                         }
-
-                        unset($serverChanges[$id]);    
                     }
-
+                }
+                
+                if ($responses->hasChildNodes() === true) {
+                    $collection->appendChild($responses);
+                }
+                
+                if ((count($serverAdds) + count($serverChanges) + count($serverDeletes)) > $collectionData['windowSize'] ) {
+                    $moreAvailable = true;
+                    $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'MoreAvailable'));
+                }
+                
+                $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
+                
+                
+                /**
+                 * process entries added on server side
+                 */
+                $newContentStates = array();
+                
+                foreach($serverAdds as $id => $serverId) {
+                    if($this->_totalCount === $collectionData['windowSize']) {
+                        break;
+                    }
+                    
                     /**
-                     * process entries deleted on server side
+                     * somewhere is a problem in the logic for handling moreAvailable
+                     * 
+                     * it can happen, that we have a contentstate (which means we sent the entry to the client
+                     * and that this entry is yet in $collectionData['syncState']->pendingdata['serverAdds']
+                     * I have no idea how this can happen, but the next lines of code work around this problem
                      */
-                    $deletedContentStates = array();
+                    try {
+                        $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
                     
-                    foreach($serverDeletes as $id => $serverId) {
-                        if($this->_totalCount === $collectionData['windowSize']) {
-                            break;
-                        }
-                                                    
-                        try {
-                            // check if we have sent this entry to the phone
-                            $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
-                            
-                            $delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
-                            $delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
-                            
-                            $deletedContentStates[] = $state;
-                            
-                            $commands->appendChild($delete);
-                            
-                            $this->_totalCount++;
-                        } catch (Exception $e) {
-                            if ($this->_logger instanceof Zend_Log) 
-                                $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
-                        }
+                        if ($this->_logger instanceof Zend_Log) 
+                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . " skipped an entry($serverId) which is already on the client");
                         
-                        unset($serverDeletes[$id]);    
+                        unset($serverAdds[$id]);
+                        continue;
+                        
+                    } catch (Syncope_Exception_NotFound $senf) {
+                        // do nothing => content state should not exist yet
                     }
                     
-                    if ($commands->hasChildNodes() === true) {
-                        $collection->appendChild($commands);
+                    try {
+                        $add = $this->_outputDom->createElementNS('uri:AirSync', 'Add');
+                        $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                        
+                        $applicationData = $add->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
+                        $dataController->appendXML($applicationData, $collectionData, $serverId);
+
+                        $commands->appendChild($add);
+                        
+                        $this->_totalCount++;
+                    } catch (Exception $e) {
+                        if ($this->_logger instanceof Zend_Log) 
+                            $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
                     }
                     
-                    if ($this->_logger instanceof Zend_Log) 
-                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData['syncState']->counter);                
+                    // mark as send to the client, even the conversion to xml might have failed 
+                    $newContentStates[] = new Syncope_Model_Content(array(
+                        'device_id'     => $this->_device,
+                        'folder_id'     => $collectionData['folder'],
+                        'contentid'     => $serverId,
+                        'creation_time' => $this->_syncTimeStamp
+                    ));
+                    unset($serverAdds[$id]);    
                 }
-                
-                if (isset($collectionData['syncState']) && $collectionData['syncState'] instanceof Syncope_Model_ISyncState && 
-                    $collectionData['syncState']->counter != $collectionData['syncKey']) {
-                    // increment sync timestamp by 1 second
-                    $this->_syncTimeStamp->modify('+1 sec');
-                    
-                    // store pending data in sync state when needed
-                    if(isset($moreAvailable) && $moreAvailable === true) {
-                        $collectionData['syncState']->pendingdata = array(
-                            'serverAdds'    => (array)$serverAdds,
-                            'serverChanges' => (array)$serverChanges,
-                            'serverDeletes' => (array)$serverDeletes
-                        );
-                    } else {
-                        $collectionData['syncState']->pendingdata = null;
+
+                /**
+                 * process entries changed on server side
+                 */
+                foreach($serverChanges as $id => $serverId) {
+                    if($this->_totalCount === $collectionData['windowSize']) {
+                        break;
                     }
-                    
-                    
-                    if (!empty($collectionData['added'])) {
+
+                    try {
+                        $change = $this->_outputDom->createElementNS('uri:AirSync', 'Change');
+                        $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                        
+                        $applicationData = $change->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ApplicationData'));
+                        $dataController->appendXML($applicationData, $collectionData, $serverId);
+
+                        $commands->appendChild($change);
+                        
+                        $this->_totalCount++;
+                    } catch (Exception $e) {
                         if ($this->_logger instanceof Zend_Log) 
-                            $this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
-                        $keepPreviousSyncKey = false;
-                    } else {
-                        $keepPreviousSyncKey = true;
+                            $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
                     }
-                    
-                    $collectionData['syncState']->lastsync = $this->_syncTimeStamp;
-                    
-                    // store new synckey
-                    $this->_syncStateBackend->create($collectionData['syncState'], $keepPreviousSyncKey);
-                    
-                    // store contentstates for new entries
-                    if (isset($newContentStates)) {
-                        foreach($newContentStates as $state) {
-                            $this->_contentStateBackend->create($state);
-                        }
-                    }
-                    
-                    // removed contentstates for deleted entries
-                    if (isset($deletedContentStates)) {
-                        foreach($deletedContentStates as $state) {
-                            $this->_contentStateBackend->delete($state);
-                        }
+
+                    unset($serverChanges[$id]);    
+                }
+
+                /**
+                 * process entries deleted on server side
+                 */
+                $deletedContentStates = array();
+                
+                foreach($serverDeletes as $id => $serverId) {
+                    if($this->_totalCount === $collectionData['windowSize']) {
+                        break;
                     }
-                    
-                    // store current filter type
+                                                
                     try {
-                        $folderState = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
-                        $folderState->lastfiltertype = $collectionData['filterType'];
-                        $this->_folderBackend->update($folderState);
-                    } catch (Syncope_Exception_NotFound $senf) {
-                        // failed to get folderstate => should not happen but is also no problem in this state
+                        // check if we have sent this entry to the phone
+                        $state = $this->_contentStateBackend->getContentState($this->_device, $collectionData['folder'], $serverId);
+                        
+                        $delete = $this->_outputDom->createElementNS('uri:AirSync', 'Delete');
+                        $delete->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'ServerId', $serverId));
+                        
+                        $deletedContentStates[] = $state;
+                        
+                        $commands->appendChild($delete);
+                        
+                        $this->_totalCount++;
+                    } catch (Exception $e) {
                         if ($this->_logger instanceof Zend_Log) 
-                            $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData['collectionId']);
+                            $this->_logger->warn(__METHOD__ . '::' . __LINE__ . " unable to convert entry to xml: " . $e->getMessage());
+                    }
+                    
+                    unset($serverDeletes[$id]);    
+                }
+                
+                if ($commands->hasChildNodes() === true) {
+                    $collection->appendChild($commands);
+                }
+                
+                if ($this->_logger instanceof Zend_Log) 
+                    $this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData['syncState']->counter);                
+            }
+            
+            if (isset($collectionData['syncState']) && $collectionData['syncState'] instanceof Syncope_Model_ISyncState && 
+                $collectionData['syncState']->counter != $collectionData['syncKey']) {
+                // increment sync timestamp by 1 second
+                $this->_syncTimeStamp->modify('+1 sec');
+                
+                // store pending data in sync state when needed
+                if(isset($moreAvailable) && $moreAvailable === true) {
+                    $collectionData['syncState']->pendingdata = array(
+                        'serverAdds'    => (array)$serverAdds,
+                        'serverChanges' => (array)$serverChanges,
+                        'serverDeletes' => (array)$serverDeletes
+                    );
+                } else {
+                    $collectionData['syncState']->pendingdata = null;
+                }
+                
+                
+                if (!empty($collectionData['added'])) {
+                    if ($this->_logger instanceof Zend_Log) 
+                        $this->_logger->info(__METHOD__ . '::' . __LINE__ . " remove previous synckey as client added new entries");
+                    $keepPreviousSyncKey = false;
+                } else {
+                    $keepPreviousSyncKey = true;
+                }
+                
+                $collectionData['syncState']->lastsync = $this->_syncTimeStamp;
+                
+                // store new synckey
+                $this->_syncStateBackend->create($collectionData['syncState'], $keepPreviousSyncKey);
+                
+                // store contentstates for new entries
+                if (isset($newContentStates)) {
+                    foreach($newContentStates as $state) {
+                        $this->_contentStateBackend->create($state);
                     }
                 }
+                
+                // removed contentstates for deleted entries
+                if (isset($deletedContentStates)) {
+                    foreach($deletedContentStates as $state) {
+                        $this->_contentStateBackend->delete($state);
+                    }
+                }
+                
+                // store current filter type
+                try {
+                    $folderState = $this->_folderBackend->getFolder($this->_device, $collectionData['collectionId']);
+                    $folderState->lastfiltertype = $collectionData['filterType'];
+                    $this->_folderBackend->update($folderState);
+                } catch (Syncope_Exception_NotFound $senf) {
+                    // failed to get folderstate => should not happen but is also no problem in this state
+                    if ($this->_logger instanceof Zend_Log) 
+                        $this->_logger->crit(__METHOD__ . '::' . __LINE__ . ' failed to get content state for: ' . $collectionData['collectionId']);
+                }
             }
         }
         
+        Syncope_Registry::getTransactionManager()->commitTransaction($this->_transactionId);
+        
         return $this->_outputDom;
     }
 }
index 05401bf..dcda501 100644 (file)
@@ -91,6 +91,10 @@ abstract class Syncope_Command_Wbxml implements Syncope_Command_Interface
     protected $_syncTimeStamp;
     
     /**
+     * @var string
+     */
+    protected $_transactionId;
+    /**
      * @var Zend_Log
      */
     protected $_logger;
index 4ecd295..04595f3 100644 (file)
@@ -126,9 +126,6 @@ class Syncope_Server
             $requestBody = $this->_body;
         }
         
-        // wrap all requests in one transaction
-        $transactionId = Syncope_Registry::getTransactionManager()->startTransaction(Syncope_Registry::getDatabase());
-        
         try {
             $command = new $className($requestBody, $device, $policyKey);
         
@@ -180,8 +177,6 @@ class Syncope_Server
             rewind($outputStream);
             fpassthru($outputStream);
         }
-        
-        Syncope_Registry::getTransactionManager()->commitTransaction($transactionId);
     }
     
     /**
index 73973f2..04f07c6 100644 (file)
@@ -50,10 +50,6 @@ class Syncope_Command_SyncTests extends Syncope_Command_ATestCase
         $xpath = new DomXPath($syncDoc);
         $xpath->registerNamespace('AirSync', 'uri:AirSync');
         
-        $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:Class');
-        $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
-        $this->assertEquals('Contacts', $nodes->item(0)->nodeValue, $syncDoc->saveXML());
-        
         $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:SyncKey');
         $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
         $this->assertEquals(0, $nodes->item(0)->nodeValue, $syncDoc->saveXML());
@@ -84,10 +80,6 @@ class Syncope_Command_SyncTests extends Syncope_Command_ATestCase
         $xpath = new DomXPath($syncDoc);
         $xpath->registerNamespace('AirSync', 'uri:AirSync');
         
-        $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:Class');
-        $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
-        $this->assertEquals('Contacts', $nodes->item(0)->nodeValue, $syncDoc->saveXML());
-        
         $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:SyncKey');
         $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
         $this->assertEquals(0, $nodes->item(0)->nodeValue, $syncDoc->saveXML());
@@ -118,10 +110,6 @@ class Syncope_Command_SyncTests extends Syncope_Command_ATestCase
         $xpath = new DomXPath($syncDoc);
         $xpath->registerNamespace('AirSync', 'uri:AirSync');
         
-        $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:Class');
-        $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
-        $this->assertEquals('Contacts', $nodes->item(0)->nodeValue, $syncDoc->saveXML());
-        
         $nodes = $xpath->query('//AirSync:Sync/AirSync:Collections/AirSync:Collection/AirSync:SyncKey');
         $this->assertEquals(1, $nodes->length, $syncDoc->saveXML());
         $this->assertEquals(0, $nodes->item(0)->nodeValue, $syncDoc->saveXML());