File indexing completed on 2024-06-23 05:55:45

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Service_WindowsAzure
0017  * @subpackage Storage
0018  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0019  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0020  * @version    $Id$
0021  */
0022 
0023 /**
0024  * @see Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
0025  */
0026 // require_once 'Zend/Service/WindowsAzure/Storage/BatchStorageAbstract.php';
0027 
0028 /**
0029  * @see Zend_Service_WindowsAzure_Storage_TableInstance
0030  */
0031 // require_once 'Zend/Service/WindowsAzure/Storage/TableInstance.php';
0032 
0033 /**
0034  * @see Zend_Service_WindowsAzure_Storage_TableEntityQuery
0035  */
0036 // require_once 'Zend/Service/WindowsAzure/Storage/TableEntityQuery.php';
0037 
0038 /**
0039  * @see Zend_Service_WindowsAzure_Storage_DynamicTableEntity
0040  */
0041 // require_once 'Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php';
0042 
0043 /**
0044  * @see Zend_Service_WindowsAzure_Credentials_SharedKeyLite 
0045  */
0046 // require_once 'Zend/Service/WindowsAzure/Credentials/SharedKeyLite.php';
0047 
0048 /**
0049  * @category   Zend
0050  * @package    Zend_Service_WindowsAzure
0051  * @subpackage Storage
0052  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0053  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0054  */
0055 class Zend_Service_WindowsAzure_Storage_Table
0056     extends Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
0057 {
0058   /**
0059    * Throw Zend_Service_WindowsAzure_Exception when a property is not specified in Windows Azure?
0060    * Defaults to true, making behaviour similar to Windows Azure StorageClient in .NET.
0061    * 
0062    * @var boolean
0063    */
0064   protected $_throwExceptionOnMissingData = true;
0065   
0066   /**
0067    * Throw Zend_Service_WindowsAzure_Exception when a property is not specified in Windows Azure?
0068    * Defaults to true, making behaviour similar to Windows Azure StorageClient in .NET.
0069    * 
0070    * @param boolean $value
0071    */
0072   public function setThrowExceptionOnMissingData($value = true)
0073   {
0074     $this->_throwExceptionOnMissingData = $value;
0075   }
0076   
0077   /**
0078    * Throw Zend_Service_WindowsAzure_Exception when a property is not specified in Windows Azure?
0079    */
0080   public function getThrowExceptionOnMissingData()
0081   {
0082     return $this->_throwExceptionOnMissingData;
0083   }
0084   
0085   /**
0086    * Creates a new Zend_Service_WindowsAzure_Storage_Table instance
0087    *
0088    * @param string $host Storage host name
0089    * @param string $accountName Account name for Windows Azure
0090    * @param string $accountKey Account key for Windows Azure
0091    * @param boolean $usePathStyleUri Use path-style URI's
0092    * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
0093    */
0094   public function __construct($host = Zend_Service_WindowsAzure_Storage::URL_DEV_TABLE, $accountName = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_ACCOUNT, $accountKey = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::DEVSTORE_KEY, $usePathStyleUri = false, Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy = null)
0095   {
0096     parent::__construct($host, $accountName, $accountKey, $usePathStyleUri, $retryPolicy);
0097 
0098       // Always use SharedKeyLite authentication
0099       $this->_credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite($accountName, $accountKey, $this->_usePathStyleUri);
0100       
0101       // API version
0102     $this->_apiVersion = '2009-09-19';
0103   }
0104   
0105   /**
0106    * Check if a table exists
0107    * 
0108    * @param string $tableName Table name
0109    * @return boolean
0110    */
0111   public function tableExists($tableName = '')
0112   {
0113     if ($tableName === '') {
0114       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0115       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0116     }
0117       
0118     // List tables
0119         $tables = $this->listTables(); // 2009-09-19 does not support $this->listTables($tableName); all of a sudden...
0120         foreach ($tables as $table) {
0121             if ($table->Name == $tableName) {
0122                 return true;
0123             }
0124         }
0125         
0126         return false;
0127   }
0128   
0129   /**
0130    * List tables
0131    *
0132    * @param  string $nextTableName Next table name, used for listing tables when total amount of tables is > 1000.
0133    * @return array
0134    * @throws Zend_Service_WindowsAzure_Exception
0135    */
0136   public function listTables($nextTableName = '')
0137   {
0138       // Build query string
0139     $queryString = array();
0140       if ($nextTableName != '') {
0141           $queryString[] = 'NextTableName=' . $nextTableName;
0142       }
0143       $queryString = self::createQueryStringFromArray($queryString);
0144       
0145     // Perform request
0146     $response = $this->_performRequest('Tables', $queryString, Zend_Http_Client::GET, null, true);
0147     if ($response->isSuccessful()) {      
0148         // Parse result
0149         $result = $this->_parseResponse($response); 
0150         
0151         if (!$result || !$result->entry) {
0152             return array();
0153         }
0154           
0155         $entries = null;
0156         if (count($result->entry) > 1) {
0157             $entries = $result->entry;
0158         } else {
0159             $entries = array($result->entry);
0160         }
0161 
0162         // Create return value
0163         $returnValue = array();       
0164         foreach ($entries as $entry) {
0165             $tableName = $entry->xpath('.//m:properties/d:TableName');
0166             $tableName = (string)$tableName[0];
0167             
0168             $returnValue[] = new Zend_Service_WindowsAzure_Storage_TableInstance(
0169                 (string)$entry->id,
0170                 $tableName,
0171                 (string)$entry->link['href'],
0172                 (string)$entry->updated
0173             );
0174         }
0175         
0176       // More tables?
0177         if (!is_null($response->getHeader('x-ms-continuation-NextTableName'))) {
0178         // require_once 'Zend/Service/WindowsAzure/Exception.php';
0179             $returnValue = array_merge($returnValue, $this->listTables($response->getHeader('x-ms-continuation-NextTableName')));
0180         }
0181 
0182         return $returnValue;
0183     } else {
0184       throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0185     }
0186   }
0187   
0188   /**
0189    * Create table
0190    *
0191    * @param string $tableName Table name
0192    * @return Zend_Service_WindowsAzure_Storage_TableInstance
0193    * @throws Zend_Service_WindowsAzure_Exception
0194    */
0195   public function createTable($tableName = '')
0196   {
0197     if ($tableName === '') {
0198       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0199     }
0200       
0201     // Generate request body
0202     $requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
0203                         <entry
0204                           xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
0205                           xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
0206                           xmlns="http://www.w3.org/2005/Atom">
0207                           <title />
0208                           <updated>{tpl:Updated}</updated>
0209                           <author>
0210                             <name />
0211                           </author>
0212                           <id />
0213                           <content type="application/xml">
0214                             <m:properties>
0215                               <d:TableName>{tpl:TableName}</d:TableName>
0216                             </m:properties>
0217                           </content>
0218                         </entry>';
0219     
0220         $requestBody = $this->_fillTemplate($requestBody, array(
0221             'BaseUrl' => $this->getBaseUrl(),
0222             'TableName' => htmlspecialchars($tableName),
0223           'Updated' => $this->isoDate(),
0224             'AccountName' => $this->_accountName
0225         ));
0226         
0227         // Add header information
0228         $headers = array();
0229         $headers['Content-Type'] = 'application/atom+xml';
0230         $headers['DataServiceVersion'] = '1.0;NetFx';
0231         $headers['MaxDataServiceVersion'] = '1.0;NetFx';        
0232 
0233     // Perform request
0234     $response = $this->_performRequest('Tables', '', Zend_Http_Client::POST, $headers, true, $requestBody);
0235     if ($response->isSuccessful()) {
0236         // Parse response
0237         $entry = $this->_parseResponse($response);
0238         
0239         $tableName = $entry->xpath('.//m:properties/d:TableName');
0240         $tableName = (string)$tableName[0];
0241             
0242       
0243         return new Zend_Service_WindowsAzure_Storage_TableInstance(
0244             (string)$entry->id,
0245             $tableName,
0246             (string)$entry->link['href'],
0247             (string)$entry->updated
0248         );
0249     } else {
0250                     // require_once 'Zend/Service/WindowsAzure/Exception.php';
0251                     throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0252     }
0253   }
0254   
0255   /**
0256    * Create table if it does not exist
0257    *
0258    * @param string $tableName Table name
0259    * @throws Zend_Service_WindowsAzure_Exception
0260    */
0261   public function createTableIfNotExists($tableName = '')
0262   {
0263     if (!$this->tableExists($tableName)) {
0264       $this->createTable($tableName);
0265     }
0266   }
0267   
0268   /**
0269    * Delete table
0270    *
0271    * @param string $tableName Table name
0272    * @throws Zend_Service_WindowsAzure_Exception
0273    */
0274   public function deleteTable($tableName = '')
0275   {
0276     if ($tableName === '') {
0277       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0278       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0279     }
0280 
0281         // Add header information
0282         $headers = array();
0283         $headers['Content-Type'] = 'application/atom+xml';
0284 
0285     // Perform request
0286     $response = $this->_performRequest('Tables(\'' . $tableName . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
0287     if (!$response->isSuccessful()) {
0288       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0289       throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0290     }
0291   }
0292   
0293   /**
0294    * Insert entity into table
0295    * 
0296    * @param string                              $tableName   Table name
0297    * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to insert
0298    * @return Zend_Service_WindowsAzure_Storage_TableEntity
0299    * @throws Zend_Service_WindowsAzure_Exception
0300    */
0301   public function insertEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null)
0302   {
0303     if ($tableName === '') {
0304       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0305       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0306     }
0307     if (is_null($entity)) {
0308       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0309       throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
0310     }
0311                          
0312     // Generate request body
0313     $requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
0314                         <entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
0315                           <title />
0316                           <updated>{tpl:Updated}</updated>
0317                           <author>
0318                             <name />
0319                           </author>
0320                           <id />
0321                           <content type="application/xml">
0322                             <m:properties>
0323                               {tpl:Properties}
0324                             </m:properties>
0325                           </content>
0326                         </entry>';
0327     
0328         $requestBody = $this->_fillTemplate($requestBody, array(
0329           'Updated'    => $this->isoDate(),
0330             'Properties' => $this->_generateAzureRepresentation($entity)
0331         ));
0332 
0333         // Add header information
0334         $headers = array();
0335         $headers['Content-Type'] = 'application/atom+xml';
0336 
0337     // Perform request
0338       $response = null;
0339       if ($this->isInBatch()) {
0340         $this->getCurrentBatch()->enlistOperation($tableName, '', Zend_Http_Client::POST, $headers, true, $requestBody);
0341         return null;
0342     } else {
0343         $response = $this->_performRequest($tableName, '', Zend_Http_Client::POST, $headers, true, $requestBody);
0344     }
0345     if ($response->isSuccessful()) {
0346         // Parse result
0347         $result = $this->_parseResponse($response);
0348         
0349         $timestamp = $result->xpath('//m:properties/d:Timestamp');
0350         $timestamp = $this->_convertToDateTime( (string)$timestamp[0] );
0351 
0352         $etag      = $result->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
0353         $etag      = (string)$etag['etag'];
0354         
0355         // Update properties
0356         $entity->setTimestamp($timestamp);
0357         $entity->setEtag($etag);
0358 
0359         return $entity;
0360     } else {
0361       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0362       throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0363     }
0364   }
0365   
0366   /**
0367    * Delete entity from table
0368    * 
0369    * @param string                              $tableName   Table name
0370    * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to delete
0371    * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
0372    * @throws Zend_Service_WindowsAzure_Exception
0373    */
0374   public function deleteEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
0375   {
0376     if ($tableName === '') {
0377       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0378       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0379     }
0380     if (is_null($entity)) {
0381       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0382       throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
0383     }
0384                          
0385         // Add header information
0386         $headers = array();
0387         if (!$this->isInBatch()) {
0388           // http://social.msdn.microsoft.com/Forums/en-US/windowsazure/thread/9e255447-4dc7-458a-99d3-bdc04bdc5474/
0389             $headers['Content-Type']   = 'application/atom+xml';
0390         }
0391         $headers['Content-Length'] = 0;
0392         if (!$verifyEtag) {
0393             $headers['If-Match']       = '*';
0394         } else {
0395             $headers['If-Match']       = $entity->getEtag();
0396         }
0397 
0398     // Perform request
0399       $response = null;
0400       if ($this->isInBatch()) {
0401         $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
0402         return null;
0403     } else {
0404         $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
0405     }
0406     if (!$response->isSuccessful()) {
0407       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0408         throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0409     }
0410   }
0411   
0412   /**
0413    * Retrieve entity from table, by id
0414    * 
0415    * @param string $tableName    Table name
0416    * @param string $partitionKey Partition key
0417    * @param string $rowKey       Row key
0418    * @param string $entityClass  Entity class name* 
0419    * @return Zend_Service_WindowsAzure_Storage_TableEntity
0420    * @throws Zend_Service_WindowsAzure_Exception
0421    */
0422   public function retrieveEntityById($tableName, $partitionKey, $rowKey, $entityClass = 'Zend_Service_WindowsAzure_Storage_DynamicTableEntity')
0423   {
0424     if (is_null($tableName) || $tableName === '') {
0425       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0426       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0427     }
0428     if (is_null($partitionKey) || $partitionKey === '') {
0429       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0430       throw new Zend_Service_WindowsAzure_Exception('Partition key is not specified.');
0431     }
0432     if (is_null($rowKey) || $rowKey === '') {
0433       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0434       throw new Zend_Service_WindowsAzure_Exception('Row key is not specified.');
0435     }
0436     if (is_null($entityClass) || $entityClass === '') {
0437       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0438       throw new Zend_Service_WindowsAzure_Exception('Entity class is not specified.');
0439     }
0440 
0441       
0442     // Check for combined size of partition key and row key
0443     // http://msdn.microsoft.com/en-us/library/dd179421.aspx
0444     if (strlen($partitionKey . $rowKey) >= 256) {
0445         // Start a batch if possible
0446         if ($this->isInBatch()) {
0447         // require_once 'Zend/Service/WindowsAzure/Exception.php';
0448             throw new Zend_Service_WindowsAzure_Exception('Entity cannot be retrieved. A transaction is required to retrieve the entity, but another transaction is already active.');
0449         }
0450             
0451         $this->startBatch();
0452     }
0453     
0454     // Fetch entities from Azure
0455         $result = $this->retrieveEntities(
0456             $this->select()
0457                  ->from($tableName)
0458                  ->wherePartitionKey($partitionKey)
0459                  ->whereRowKey($rowKey),
0460             '',
0461             $entityClass
0462         );
0463         
0464         // Return
0465         if (count($result) == 1) {
0466             return $result[0];
0467         }
0468         
0469         return null;
0470   }
0471   
0472   /**
0473    * Create a new Zend_Service_WindowsAzure_Storage_TableEntityQuery
0474    * 
0475    * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
0476    */
0477   public function select()
0478   {
0479             
0480       return new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
0481   }
0482   
0483   /**
0484    * Retrieve entities from table
0485    * 
0486    * @param string $tableName|Zend_Service_WindowsAzure_Storage_TableEntityQuery    Table name -or- Zend_Service_WindowsAzure_Storage_TableEntityQuery instance
0487    * @param string $filter                                                Filter condition (not applied when $tableName is a Zend_Service_WindowsAzure_Storage_TableEntityQuery instance)
0488    * @param string $entityClass                                           Entity class name
0489    * @param string $nextPartitionKey                                      Next partition key, used for listing entities when total amount of entities is > 1000.
0490    * @param string $nextRowKey                                            Next row key, used for listing entities when total amount of entities is > 1000.
0491    * @return array Array of Zend_Service_WindowsAzure_Storage_TableEntity
0492    * @throws Zend_Service_WindowsAzure_Exception
0493    */
0494   public function retrieveEntities($tableName = '', $filter = '', $entityClass = 'Zend_Service_WindowsAzure_Storage_DynamicTableEntity', $nextPartitionKey = null, $nextRowKey = null)
0495   {
0496     if ($tableName === '') {
0497       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0498       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0499     }
0500     if ($entityClass === '') {
0501       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0502       throw new Zend_Service_WindowsAzure_Exception('Entity class is not specified.');
0503     }
0504 
0505     // Convenience...
0506     if (class_exists($filter)) {
0507         $entityClass = $filter;
0508         $filter = '';
0509     }
0510       
0511     // Query string
0512     $queryString = '';
0513 
0514     // Determine query
0515     if (is_string($tableName)) {
0516         // Option 1: $tableName is a string
0517         
0518         // Append parentheses
0519         if (strpos($tableName, '()') === false) {
0520           $tableName .= '()';
0521         }
0522         
0523           // Build query
0524           $query = array();
0525           
0526         // Filter?
0527         if ($filter !== '') {
0528             $query[] = '$filter=' . Zend_Service_WindowsAzure_Storage_TableEntityQuery::encodeQuery($filter);
0529         }
0530             
0531           // Build queryString
0532           if (count($query) > 0)  {
0533               $queryString = '?' . implode('&', $query);
0534           }
0535     } else if (get_class($tableName) == 'Zend_Service_WindowsAzure_Storage_TableEntityQuery') {
0536         // Option 2: $tableName is a Zend_Service_WindowsAzure_Storage_TableEntityQuery instance
0537 
0538         // Build queryString
0539         $queryString = $tableName->assembleQueryString(true);
0540 
0541         // Change $tableName
0542         $tableName = $tableName->assembleFrom(true);
0543     } else {
0544       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0545         throw new Zend_Service_WindowsAzure_Exception('Invalid argument: $tableName');
0546     }
0547     
0548     // Add continuation querystring parameters?
0549     if (!is_null($nextPartitionKey) && !is_null($nextRowKey)) {
0550         if ($queryString !== '') {
0551             $queryString .= '&';
0552         } else {
0553           $queryString .= '?';
0554         }
0555             
0556         $queryString .= 'NextPartitionKey=' . rawurlencode($nextPartitionKey) . '&NextRowKey=' . rawurlencode($nextRowKey);
0557     }
0558 
0559     // Perform request
0560       $response = null;
0561       if ($this->isInBatch() && $this->getCurrentBatch()->getOperationCount() == 0) {
0562         $this->getCurrentBatch()->enlistOperation($tableName, $queryString, Zend_Http_Client::GET, array(), true, null);
0563         $response = $this->getCurrentBatch()->commit();
0564         
0565         // Get inner response (multipart)
0566         $innerResponse = $response->getBody();
0567         $innerResponse = substr($innerResponse, strpos($innerResponse, 'HTTP/1.1 200 OK'));
0568         $innerResponse = substr($innerResponse, 0, strpos($innerResponse, '--batchresponse'));
0569         $response = Zend_Http_Response::fromString($innerResponse);
0570     } else {
0571         $response = $this->_performRequest($tableName, $queryString, Zend_Http_Client::GET, array(), true, null);
0572     }
0573 
0574     if ($response->isSuccessful()) {
0575         // Parse result
0576         $result = $this->_parseResponse($response);
0577         if (!$result) {
0578             return array();
0579         }
0580 
0581         $entries = null;
0582         if ($result->entry) {
0583             if (count($result->entry) > 1) {
0584                 $entries = $result->entry;
0585             } else {
0586                 $entries = array($result->entry);
0587             }
0588         } else {
0589             // This one is tricky... If we have properties defined, we have an entity.
0590             $properties = $result->xpath('//m:properties');
0591             if ($properties) {
0592                 $entries = array($result);
0593             } else {
0594                 return array();
0595             }
0596         }
0597 
0598         // Create return value
0599         $returnValue = array();       
0600         foreach ($entries as $entry) {
0601             // Parse properties
0602             $properties = $entry->xpath('.//m:properties');
0603             $properties = $properties[0]->children('http://schemas.microsoft.com/ado/2007/08/dataservices');
0604             
0605             // Create entity
0606             $entity = new $entityClass('', '');
0607             $entity->setAzureValues((array)$properties, $this->_throwExceptionOnMissingData);
0608             
0609             // If we have a Zend_Service_WindowsAzure_Storage_DynamicTableEntity, make sure all property types are set
0610             if ($entity instanceof Zend_Service_WindowsAzure_Storage_DynamicTableEntity) {
0611                 foreach ($properties as $key => $value) {  
0612                     $attributes = $value->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
0613                     $type = (string)$attributes['type'];
0614                     if ($type !== '') {
0615                       $entity->setAzureProperty($key, (string)$value, $type);
0616                     }
0617                 }
0618             }
0619     
0620             // Update etag
0621             $etag      = $entry->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
0622             $etag      = (string)$etag['etag'];
0623             $entity->setEtag($etag);
0624             
0625             // Add to result
0626             $returnValue[] = $entity;
0627         }
0628 
0629       // More entities?
0630         if (!is_null($response->getHeader('x-ms-continuation-NextPartitionKey')) && !is_null($response->getHeader('x-ms-continuation-NextRowKey'))) {
0631             if (strpos($queryString, '$top') === false) {
0632                 $returnValue = array_merge($returnValue, $this->retrieveEntities($tableName, $filter, $entityClass, $response->getHeader('x-ms-continuation-NextPartitionKey'), $response->getHeader('x-ms-continuation-NextRowKey')));
0633             }
0634         }
0635         
0636         // Return
0637         return $returnValue;
0638     } else {
0639       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0640         throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0641     }
0642   }
0643   
0644   /**
0645    * Update entity by replacing it
0646    * 
0647    * @param string                              $tableName   Table name
0648    * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
0649    * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
0650    * @throws Zend_Service_WindowsAzure_Exception
0651    */
0652   public function updateEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
0653   {
0654       return $this->_changeEntity(Zend_Http_Client::PUT, $tableName, $entity, $verifyEtag);
0655   }
0656   
0657   /**
0658    * Update entity by adding or updating properties
0659    * 
0660    * @param string                              $tableName   Table name
0661    * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
0662    * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
0663    * @param array                               $properties  Properties to merge. All properties will be used when omitted.
0664    * @throws Zend_Service_WindowsAzure_Exception
0665    */
0666   public function mergeEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false, $properties = array())
0667   {
0668     $mergeEntity = null;
0669     if (is_array($properties) && count($properties) > 0) {
0670       
0671       // Build a new object
0672       $mergeEntity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($entity->getPartitionKey(), $entity->getRowKey());
0673       
0674       // Keep only values mentioned in $properties
0675       $azureValues = $entity->getAzureValues();
0676       foreach ($azureValues as $key => $value) {
0677         if (in_array($value->Name, $properties)) {
0678           $mergeEntity->setAzureProperty($value->Name, $value->Value, $value->Type);
0679         }
0680       }
0681     } else {
0682       $mergeEntity = $entity;
0683     }
0684     
0685     // Ensure entity timestamp matches updated timestamp 
0686         $entity->setTimestamp(new DateTime());
0687         
0688       return $this->_changeEntity(Zend_Http_Client::MERGE, $tableName, $mergeEntity, $verifyEtag);
0689   }
0690   
0691   /**
0692    * Get error message from Zend_Http_Response
0693    * 
0694    * @param Zend_Http_Response $response Repsonse
0695    * @param string $alternativeError Alternative error message
0696    * @return string
0697    */
0698   protected function _getErrorMessage(Zend_Http_Response $response, $alternativeError = 'Unknown error.')
0699   {
0700     $response = $this->_parseResponse($response);
0701     if ($response && $response->message) {
0702         return (string)$response->message;
0703     } else {
0704         return $alternativeError;
0705     }
0706   }
0707   
0708   /**
0709    * Update entity / merge entity
0710    * 
0711    * @param string                              $httpVerb    HTTP verb to use (PUT = update, MERGE = merge)
0712    * @param string                              $tableName   Table name
0713    * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
0714    * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
0715    * @throws Zend_Service_WindowsAzure_Exception
0716    */
0717   protected function _changeEntity($httpVerb = Zend_Http_Client::PUT, $tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
0718   {
0719     if ($tableName === '') {
0720       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0721       throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
0722     }
0723     if (is_null($entity)) { 
0724       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0725       throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
0726     }
0727                          
0728         // Add header information
0729         $headers = array();
0730         $headers['Content-Type']   = 'application/atom+xml';
0731         $headers['Content-Length'] = 0;
0732         if (!$verifyEtag) {
0733             $headers['If-Match']       = '*';
0734         } else {
0735             $headers['If-Match']       = $entity->getEtag();
0736         }
0737 
0738       // Generate request body
0739     $requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
0740                         <entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
0741                           <title />
0742                           <updated>{tpl:Updated}</updated>
0743                           <author>
0744                             <name />
0745                           </author>
0746                           <id />
0747                           <content type="application/xml">
0748                             <m:properties>
0749                               {tpl:Properties}
0750                             </m:properties>
0751                           </content>
0752                         </entry>';
0753     
0754     // Attempt to get timestamp from entity
0755         $timestamp = $entity->getTimestamp();
0756         
0757         $requestBody = $this->_fillTemplate($requestBody, array(
0758           'Updated'    => $this->_convertToEdmDateTime($timestamp),
0759             'Properties' => $this->_generateAzureRepresentation($entity)
0760         ));
0761 
0762         // Add header information
0763         $headers = array();
0764         $headers['Content-Type'] = 'application/atom+xml';
0765       if (!$verifyEtag) {
0766             $headers['If-Match']       = '*';
0767         } else {
0768             $headers['If-Match']       = $entity->getEtag();
0769         }
0770         
0771     // Perform request
0772     $response = null;
0773       if ($this->isInBatch()) {
0774         $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
0775         return null;
0776     } else {
0777         $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
0778     }
0779     if ($response->isSuccessful()) {
0780         // Update properties
0781       $entity->setEtag($response->getHeader('Etag'));
0782       $entity->setTimestamp( $this->_convertToDateTime($response->getHeader('Last-modified')) );
0783 
0784         return $entity;
0785     } else {
0786       // require_once 'Zend/Service/WindowsAzure/Exception.php';
0787       throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
0788     }
0789   }
0790   
0791   /**
0792    * Generate RFC 1123 compliant date string
0793    * 
0794    * @return string
0795    */
0796   protected function _rfcDate()
0797   {
0798       return gmdate('D, d M Y H:i:s', time()) . ' GMT'; // RFC 1123
0799   }
0800   
0801   /**
0802    * Fill text template with variables from key/value array
0803    * 
0804    * @param string $templateText Template text
0805    * @param array $variables Array containing key/value pairs
0806    * @return string
0807    */
0808   protected function _fillTemplate($templateText, $variables = array())
0809   {
0810       foreach ($variables as $key => $value) {
0811           $templateText = str_replace('{tpl:' . $key . '}', $value, $templateText);
0812       }
0813       return $templateText;
0814   }
0815   
0816   /**
0817    * Generate Azure representation from entity (creates atompub markup from properties)
0818    * 
0819    * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity
0820    * @return string
0821    */
0822   protected function _generateAzureRepresentation(Zend_Service_WindowsAzure_Storage_TableEntity $entity = null)
0823   {
0824     // Generate Azure representation from entity
0825     $azureRepresentation = array();
0826     $azureValues         = $entity->getAzureValues();
0827     foreach ($azureValues as $azureValue) {
0828         $value = array();
0829         $value[] = '<d:' . $azureValue->Name;
0830         if ($azureValue->Type != '') {
0831             $value[] = ' m:type="' . $azureValue->Type . '"';
0832         }
0833         if (is_null($azureValue->Value)) {
0834             $value[] = ' m:null="true"'; 
0835         }
0836         $value[] = '>';
0837         
0838         if (!is_null($azureValue->Value)) {
0839             if (strtolower($azureValue->Type) == 'edm.boolean') {
0840                 $value[] = ($azureValue->Value == true ? '1' : '0');
0841             } else if (strtolower($azureValue->Type) == 'edm.datetime') {
0842               $value[] = $this->_convertToEdmDateTime($azureValue->Value);
0843             } else {
0844                 $value[] = htmlspecialchars($azureValue->Value);
0845             }
0846         }
0847         
0848         $value[] = '</d:' . $azureValue->Name . '>';
0849         $azureRepresentation[] = implode('', $value);
0850     }
0851 
0852     return implode('', $azureRepresentation);
0853   }
0854   
0855     /**
0856    * Perform request using Zend_Http_Client channel
0857    *
0858    * @param string $path Path
0859    * @param string $queryString Query string
0860    * @param string $httpVerb HTTP verb the request will use
0861    * @param array $headers x-ms headers to add
0862    * @param boolean $forTableStorage Is the request for table storage?
0863    * @param mixed $rawData Optional RAW HTTP data to be sent over the wire
0864    * @param string $resourceType Resource type
0865    * @param string $requiredPermission Required permission
0866    * @return Zend_Http_Response
0867    */
0868   protected function _performRequest(
0869     $path = '/',
0870     $queryString = '',
0871     $httpVerb = Zend_Http_Client::GET,
0872     $headers = array(),
0873     $forTableStorage = false,
0874     $rawData = null,
0875     $resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN,
0876     $requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
0877   ) {
0878     // Add headers
0879     $headers['DataServiceVersion'] = '1.0;NetFx';
0880     $headers['MaxDataServiceVersion'] = '1.0;NetFx';
0881 
0882     // Perform request
0883     return parent::_performRequest(
0884       $path,
0885       $queryString,
0886       $httpVerb,
0887       $headers,
0888       $forTableStorage,
0889       $rawData,
0890       $resourceType,
0891       $requiredPermission
0892     );
0893   }  
0894     
0895     /**
0896      * Converts a string to a DateTime object. Returns false on failure.
0897      * 
0898      * @param string $value The string value to parse
0899      * @return DateTime|boolean
0900      */
0901     protected function _convertToDateTime($value = '') 
0902     {
0903       if ($value instanceof DateTime) {
0904         return $value;
0905       }
0906       
0907       try {
0908         if (substr($value, -1) == 'Z') {
0909           $value = substr($value, 0, strlen($value) - 1);
0910         }
0911         return new DateTime($value, new DateTimeZone('UTC'));
0912       }
0913       catch (Exception $ex) {
0914         return false;
0915       }
0916     }
0917     
0918     /**
0919      * Converts a DateTime object into an Edm.DaeTime value in UTC timezone,
0920      * represented as a string.
0921      * 
0922      * @param DateTime $value
0923      * @return string
0924      */
0925     protected function _convertToEdmDateTime(DateTime $value) 
0926     {
0927       $cloned = clone $value;
0928       $cloned->setTimezone(new DateTimeZone('UTC'));
0929       return str_replace('+0000', 'Z', $cloned->format(DateTime::ISO8601));
0930     }
0931 }