PageRenderTime 67ms CodeModel.GetById 21ms app.highlight 35ms RepoModel.GetById 1ms app.codeStats 0ms

/Zend/Service/WindowsAzure/Storage/Table.php

http://grupal.googlecode.com/
PHP | 931 lines | 547 code | 104 blank | 280 comment | 108 complexity | a9b2e1693cf403ae8c02fe88d7e985ce MD5 | raw file
  1<?php
  2/**
  3 * Zend Framework
  4 *
  5 * LICENSE
  6 *
  7 * This source file is subject to the new BSD license that is bundled
  8 * with this package in the file LICENSE.txt.
  9 * It is also available through the world-wide-web at this URL:
 10 * http://framework.zend.com/license/new-bsd
 11 * If you did not receive a copy of the license and are unable to
 12 * obtain it through the world-wide-web, please send an email
 13 * to license@zend.com so we can send you a copy immediately.
 14 *
 15 * @category   Zend
 16 * @package    Zend_Service_WindowsAzure
 17 * @subpackage Storage
 18 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 19 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 20 * @version    $Id: Table.php 24697 2012-03-23 13:11:04Z ezimuel $
 21 */
 22
 23/**
 24 * @see Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
 25 */
 26require_once 'Zend/Service/WindowsAzure/Storage/BatchStorageAbstract.php';
 27
 28/**
 29 * @see Zend_Service_WindowsAzure_Storage_TableInstance
 30 */
 31require_once 'Zend/Service/WindowsAzure/Storage/TableInstance.php';
 32
 33/**
 34 * @see Zend_Service_WindowsAzure_Storage_TableEntityQuery
 35 */
 36require_once 'Zend/Service/WindowsAzure/Storage/TableEntityQuery.php';
 37
 38/**
 39 * @see Zend_Service_WindowsAzure_Storage_DynamicTableEntity
 40 */
 41require_once 'Zend/Service/WindowsAzure/Storage/DynamicTableEntity.php';
 42
 43/**
 44 * @see Zend_Service_WindowsAzure_Credentials_SharedKeyLite 
 45 */
 46require_once 'Zend/Service/WindowsAzure/Credentials/SharedKeyLite.php';
 47
 48/**
 49 * @category   Zend
 50 * @package    Zend_Service_WindowsAzure
 51 * @subpackage Storage
 52 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 53 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 54 */
 55class Zend_Service_WindowsAzure_Storage_Table
 56    extends Zend_Service_WindowsAzure_Storage_BatchStorageAbstract
 57{
 58	/**
 59	 * Throw Zend_Service_WindowsAzure_Exception when a property is not specified in Windows Azure?
 60	 * Defaults to true, making behaviour similar to Windows Azure StorageClient in .NET.
 61	 * 
 62	 * @var boolean
 63	 */
 64	protected $_throwExceptionOnMissingData = true;
 65	
 66	/**
 67	 * Throw Zend_Service_WindowsAzure_Exception when a property is not specified in Windows Azure?
 68	 * Defaults to true, making behaviour similar to Windows Azure StorageClient in .NET.
 69	 * 
 70	 * @param boolean $value
 71	 */
 72	public function setThrowExceptionOnMissingData($value = true)
 73	{
 74		$this->_throwExceptionOnMissingData = $value;
 75	}
 76	
 77	/**
 78	 * Throw Zend_Service_WindowsAzure_Exception when a property is not specified in Windows Azure?
 79	 */
 80	public function getThrowExceptionOnMissingData()
 81	{
 82		return $this->_throwExceptionOnMissingData;
 83	}
 84	
 85	/**
 86	 * Creates a new Zend_Service_WindowsAzure_Storage_Table instance
 87	 *
 88	 * @param string $host Storage host name
 89	 * @param string $accountName Account name for Windows Azure
 90	 * @param string $accountKey Account key for Windows Azure
 91	 * @param boolean $usePathStyleUri Use path-style URI's
 92	 * @param Zend_Service_WindowsAzure_RetryPolicy_RetryPolicyAbstract $retryPolicy Retry policy to use when making requests
 93	 */
 94	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)
 95	{
 96		parent::__construct($host, $accountName, $accountKey, $usePathStyleUri, $retryPolicy);
 97
 98	    // Always use SharedKeyLite authentication
 99	    $this->_credentials = new Zend_Service_WindowsAzure_Credentials_SharedKeyLite($accountName, $accountKey, $this->_usePathStyleUri);
100	    
101	    // API version
102		$this->_apiVersion = '2009-09-19';
103	}
104	
105	/**
106	 * Check if a table exists
107	 * 
108	 * @param string $tableName Table name
109	 * @return boolean
110	 */
111	public function tableExists($tableName = '')
112	{
113		if ($tableName === '') {
114			require_once 'Zend/Service/WindowsAzure/Exception.php';
115			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
116		}
117			
118		// List tables
119        $tables = $this->listTables(); // 2009-09-19 does not support $this->listTables($tableName); all of a sudden...
120        foreach ($tables as $table) {
121            if ($table->Name == $tableName) {
122                return true;
123            }
124        }
125        
126        return false;
127	}
128	
129	/**
130	 * List tables
131	 *
132	 * @param  string $nextTableName Next table name, used for listing tables when total amount of tables is > 1000.
133	 * @return array
134	 * @throws Zend_Service_WindowsAzure_Exception
135	 */
136	public function listTables($nextTableName = '')
137	{
138	    // Build query string
139		$queryString = array();
140	    if ($nextTableName != '') {
141	        $queryString[] = 'NextTableName=' . $nextTableName;
142	    }
143	    $queryString = self::createQueryStringFromArray($queryString);
144	    
145		// Perform request
146		$response = $this->_performRequest('Tables', $queryString, Zend_Http_Client::GET, null, true);
147		if ($response->isSuccessful()) {	    
148		    // Parse result
149		    $result = $this->_parseResponse($response);	
150		    
151		    if (!$result || !$result->entry) {
152		        return array();
153		    }
154	        
155		    $entries = null;
156		    if (count($result->entry) > 1) {
157		        $entries = $result->entry;
158		    } else {
159		        $entries = array($result->entry);
160		    }
161
162		    // Create return value
163		    $returnValue = array();		    
164		    foreach ($entries as $entry) {
165		        $tableName = $entry->xpath('.//m:properties/d:TableName');
166		        $tableName = (string)$tableName[0];
167		        
168		        $returnValue[] = new Zend_Service_WindowsAzure_Storage_TableInstance(
169		            (string)$entry->id,
170		            $tableName,
171		            (string)$entry->link['href'],
172		            (string)$entry->updated
173		        );
174		    }
175		    
176			// More tables?
177		    if (!is_null($response->getHeader('x-ms-continuation-NextTableName'))) {
178				require_once 'Zend/Service/WindowsAzure/Exception.php';
179		        $returnValue = array_merge($returnValue, $this->listTables($response->getHeader('x-ms-continuation-NextTableName')));
180		    }
181
182		    return $returnValue;
183		} else {
184			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
185		}
186	}
187	
188	/**
189	 * Create table
190	 *
191	 * @param string $tableName Table name
192	 * @return Zend_Service_WindowsAzure_Storage_TableInstance
193	 * @throws Zend_Service_WindowsAzure_Exception
194	 */
195	public function createTable($tableName = '')
196	{
197		if ($tableName === '') {
198			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
199		}
200			
201		// Generate request body
202		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
203                        <entry
204                        	xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
205                        	xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
206                        	xmlns="http://www.w3.org/2005/Atom">
207                          <title />
208                          <updated>{tpl:Updated}</updated>
209                          <author>
210                            <name />
211                          </author>
212                          <id />
213                          <content type="application/xml">
214                            <m:properties>
215                              <d:TableName>{tpl:TableName}</d:TableName>
216                            </m:properties>
217                          </content>
218                        </entry>';
219		
220        $requestBody = $this->_fillTemplate($requestBody, array(
221            'BaseUrl' => $this->getBaseUrl(),
222            'TableName' => htmlspecialchars($tableName),
223        	'Updated' => $this->isoDate(),
224            'AccountName' => $this->_accountName
225        ));
226        
227        // Add header information
228        $headers = array();
229        $headers['Content-Type'] = 'application/atom+xml';
230        $headers['DataServiceVersion'] = '1.0;NetFx';
231        $headers['MaxDataServiceVersion'] = '1.0;NetFx';        
232
233		// Perform request
234		$response = $this->_performRequest('Tables', '', Zend_Http_Client::POST, $headers, true, $requestBody);
235		if ($response->isSuccessful()) {
236		    // Parse response
237		    $entry = $this->_parseResponse($response);
238		    
239		    $tableName = $entry->xpath('.//m:properties/d:TableName');
240		    $tableName = (string)$tableName[0];
241		        
242			
243		    return new Zend_Service_WindowsAzure_Storage_TableInstance(
244		        (string)$entry->id,
245		        $tableName,
246		        (string)$entry->link['href'],
247		        (string)$entry->updated
248		    );
249		} else {
250                    require_once 'Zend/Service/WindowsAzure/Exception.php';
251                    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
252		}
253	}
254	
255	/**
256	 * Create table if it does not exist
257	 *
258	 * @param string $tableName Table name
259	 * @throws Zend_Service_WindowsAzure_Exception
260	 */
261	public function createTableIfNotExists($tableName = '')
262	{
263		if (!$this->tableExists($tableName)) {
264			$this->createTable($tableName);
265		}
266	}
267	
268	/**
269	 * Delete table
270	 *
271	 * @param string $tableName Table name
272	 * @throws Zend_Service_WindowsAzure_Exception
273	 */
274	public function deleteTable($tableName = '')
275	{
276		if ($tableName === '') {
277			require_once 'Zend/Service/WindowsAzure/Exception.php';
278			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
279		}
280
281        // Add header information
282        $headers = array();
283        $headers['Content-Type'] = 'application/atom+xml';
284
285		// Perform request
286		$response = $this->_performRequest('Tables(\'' . $tableName . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
287		if (!$response->isSuccessful()) {
288			require_once 'Zend/Service/WindowsAzure/Exception.php';
289			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
290		}
291	}
292	
293	/**
294	 * Insert entity into table
295	 * 
296	 * @param string                              $tableName   Table name
297	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to insert
298	 * @return Zend_Service_WindowsAzure_Storage_TableEntity
299	 * @throws Zend_Service_WindowsAzure_Exception
300	 */
301	public function insertEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null)
302	{
303		if ($tableName === '') {
304			require_once 'Zend/Service/WindowsAzure/Exception.php';
305			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
306		}
307		if (is_null($entity)) {
308			require_once 'Zend/Service/WindowsAzure/Exception.php';
309			throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
310		}
311		                     
312		// Generate request body
313		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
314                        <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">
315                          <title />
316                          <updated>{tpl:Updated}</updated>
317                          <author>
318                            <name />
319                          </author>
320                          <id />
321                          <content type="application/xml">
322                            <m:properties>
323                              {tpl:Properties}
324                            </m:properties>
325                          </content>
326                        </entry>';
327		
328        $requestBody = $this->_fillTemplate($requestBody, array(
329        	'Updated'    => $this->isoDate(),
330            'Properties' => $this->_generateAzureRepresentation($entity)
331        ));
332
333        // Add header information
334        $headers = array();
335        $headers['Content-Type'] = 'application/atom+xml';
336
337		// Perform request
338	    $response = null;
339	    if ($this->isInBatch()) {
340		    $this->getCurrentBatch()->enlistOperation($tableName, '', Zend_Http_Client::POST, $headers, true, $requestBody);
341		    return null;
342		} else {
343		    $response = $this->_performRequest($tableName, '', Zend_Http_Client::POST, $headers, true, $requestBody);
344		}
345		if ($response->isSuccessful()) {
346		    // Parse result
347		    $result = $this->_parseResponse($response);
348		    
349		    $timestamp = $result->xpath('//m:properties/d:Timestamp');
350		    $timestamp = $this->_convertToDateTime( (string)$timestamp[0] );
351
352		    $etag      = $result->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
353		    $etag      = (string)$etag['etag'];
354		    
355		    // Update properties
356		    $entity->setTimestamp($timestamp);
357		    $entity->setEtag($etag);
358
359		    return $entity;
360		} else {
361			require_once 'Zend/Service/WindowsAzure/Exception.php';
362			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
363		}
364	}
365	
366	/**
367	 * Delete entity from table
368	 * 
369	 * @param string                              $tableName   Table name
370	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to delete
371	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
372	 * @throws Zend_Service_WindowsAzure_Exception
373	 */
374	public function deleteEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
375	{
376		if ($tableName === '') {
377			require_once 'Zend/Service/WindowsAzure/Exception.php';
378			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
379		}
380		if (is_null($entity)) {
381			require_once 'Zend/Service/WindowsAzure/Exception.php';
382			throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
383		}
384		                     
385        // Add header information
386        $headers = array();
387        if (!$this->isInBatch()) {
388        	// http://social.msdn.microsoft.com/Forums/en-US/windowsazure/thread/9e255447-4dc7-458a-99d3-bdc04bdc5474/
389            $headers['Content-Type']   = 'application/atom+xml';
390        }
391        $headers['Content-Length'] = 0;
392        if (!$verifyEtag) {
393            $headers['If-Match']       = '*';
394        } else {
395            $headers['If-Match']       = $entity->getEtag();
396        }
397
398		// Perform request
399	    $response = null;
400	    if ($this->isInBatch()) {
401		    $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
402		    return null;
403		} else {
404		    $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Zend_Http_Client::DELETE, $headers, true, null);
405		}
406		if (!$response->isSuccessful()) {
407			require_once 'Zend/Service/WindowsAzure/Exception.php';
408		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
409		}
410	}
411	
412	/**
413	 * Retrieve entity from table, by id
414	 * 
415	 * @param string $tableName    Table name
416	 * @param string $partitionKey Partition key
417	 * @param string $rowKey       Row key
418	 * @param string $entityClass  Entity class name* 
419	 * @return Zend_Service_WindowsAzure_Storage_TableEntity
420	 * @throws Zend_Service_WindowsAzure_Exception
421	 */
422	public function retrieveEntityById($tableName, $partitionKey, $rowKey, $entityClass = 'Zend_Service_WindowsAzure_Storage_DynamicTableEntity')
423	{
424		if (is_null($tableName) || $tableName === '') {
425			require_once 'Zend/Service/WindowsAzure/Exception.php';
426			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
427		}
428		if (is_null($partitionKey) || $partitionKey === '') {
429			require_once 'Zend/Service/WindowsAzure/Exception.php';
430			throw new Zend_Service_WindowsAzure_Exception('Partition key is not specified.');
431		}
432		if (is_null($rowKey) || $rowKey === '') {
433			require_once 'Zend/Service/WindowsAzure/Exception.php';
434			throw new Zend_Service_WindowsAzure_Exception('Row key is not specified.');
435		}
436		if (is_null($entityClass) || $entityClass === '') {
437			require_once 'Zend/Service/WindowsAzure/Exception.php';
438			throw new Zend_Service_WindowsAzure_Exception('Entity class is not specified.');
439		}
440
441			
442		// Check for combined size of partition key and row key
443		// http://msdn.microsoft.com/en-us/library/dd179421.aspx
444		if (strlen($partitionKey . $rowKey) >= 256) {
445		    // Start a batch if possible
446		    if ($this->isInBatch()) {
447				require_once 'Zend/Service/WindowsAzure/Exception.php';
448		        throw new Zend_Service_WindowsAzure_Exception('Entity cannot be retrieved. A transaction is required to retrieve the entity, but another transaction is already active.');
449		    }
450		        
451		    $this->startBatch();
452		}
453		
454		// Fetch entities from Azure
455        $result = $this->retrieveEntities(
456            $this->select()
457                 ->from($tableName)
458                 ->wherePartitionKey($partitionKey)
459                 ->whereRowKey($rowKey),
460            '',
461            $entityClass
462        );
463        
464        // Return
465        if (count($result) == 1) {
466            return $result[0];
467        }
468        
469        return null;
470	}
471	
472	/**
473	 * Create a new Zend_Service_WindowsAzure_Storage_TableEntityQuery
474	 * 
475	 * @return Zend_Service_WindowsAzure_Storage_TableEntityQuery
476	 */
477	public function select()
478	{
479            
480	    return new Zend_Service_WindowsAzure_Storage_TableEntityQuery();
481	}
482	
483	/**
484	 * Retrieve entities from table
485	 * 
486	 * @param string $tableName|Zend_Service_WindowsAzure_Storage_TableEntityQuery    Table name -or- Zend_Service_WindowsAzure_Storage_TableEntityQuery instance
487	 * @param string $filter                                                Filter condition (not applied when $tableName is a Zend_Service_WindowsAzure_Storage_TableEntityQuery instance)
488	 * @param string $entityClass                                           Entity class name
489	 * @param string $nextPartitionKey                                      Next partition key, used for listing entities when total amount of entities is > 1000.
490	 * @param string $nextRowKey                                            Next row key, used for listing entities when total amount of entities is > 1000.
491	 * @return array Array of Zend_Service_WindowsAzure_Storage_TableEntity
492	 * @throws Zend_Service_WindowsAzure_Exception
493	 */
494	public function retrieveEntities($tableName = '', $filter = '', $entityClass = 'Zend_Service_WindowsAzure_Storage_DynamicTableEntity', $nextPartitionKey = null, $nextRowKey = null)
495	{
496		if ($tableName === '') {
497			require_once 'Zend/Service/WindowsAzure/Exception.php';
498			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
499		}
500		if ($entityClass === '') {
501			require_once 'Zend/Service/WindowsAzure/Exception.php';
502			throw new Zend_Service_WindowsAzure_Exception('Entity class is not specified.');
503		}
504
505		// Convenience...
506		if (class_exists($filter)) {
507		    $entityClass = $filter;
508		    $filter = '';
509		}
510			
511		// Query string
512		$queryString = '';
513
514		// Determine query
515		if (is_string($tableName)) {
516		    // Option 1: $tableName is a string
517		    
518		    // Append parentheses
519		    if (strpos($tableName, '()') === false) {
520		    	$tableName .= '()';
521		    }
522		    
523    	    // Build query
524    	    $query = array();
525    	    
526    		// Filter?
527    		if ($filter !== '') {
528    		    $query[] = '$filter=' . Zend_Service_WindowsAzure_Storage_TableEntityQuery::encodeQuery($filter);
529    		}
530    		    
531    	    // Build queryString
532    	    if (count($query) > 0)  {
533    	        $queryString = '?' . implode('&', $query);
534    	    }
535		} else if (get_class($tableName) == 'Zend_Service_WindowsAzure_Storage_TableEntityQuery') {
536		    // Option 2: $tableName is a Zend_Service_WindowsAzure_Storage_TableEntityQuery instance
537
538		    // Build queryString
539		    $queryString = $tableName->assembleQueryString(true);
540
541		    // Change $tableName
542		    $tableName = $tableName->assembleFrom(true);
543		} else {
544			require_once 'Zend/Service/WindowsAzure/Exception.php';
545		    throw new Zend_Service_WindowsAzure_Exception('Invalid argument: $tableName');
546		}
547		
548		// Add continuation querystring parameters?
549		if (!is_null($nextPartitionKey) && !is_null($nextRowKey)) {
550		    if ($queryString !== '') {
551		        $queryString .= '&';
552		    } else {
553		    	$queryString .= '?';
554		    }
555		        
556		    $queryString .= 'NextPartitionKey=' . rawurlencode($nextPartitionKey) . '&NextRowKey=' . rawurlencode($nextRowKey);
557		}
558
559		// Perform request
560	    $response = null;
561	    if ($this->isInBatch() && $this->getCurrentBatch()->getOperationCount() == 0) {
562		    $this->getCurrentBatch()->enlistOperation($tableName, $queryString, Zend_Http_Client::GET, array(), true, null);
563		    $response = $this->getCurrentBatch()->commit();
564		    
565		    // Get inner response (multipart)
566		    $innerResponse = $response->getBody();
567		    $innerResponse = substr($innerResponse, strpos($innerResponse, 'HTTP/1.1 200 OK'));
568		    $innerResponse = substr($innerResponse, 0, strpos($innerResponse, '--batchresponse'));
569		    $response = Zend_Http_Response::fromString($innerResponse);
570		} else {
571		    $response = $this->_performRequest($tableName, $queryString, Zend_Http_Client::GET, array(), true, null);
572		}
573
574		if ($response->isSuccessful()) {
575		    // Parse result
576		    $result = $this->_parseResponse($response);
577		    if (!$result) {
578		        return array();
579		    }
580
581		    $entries = null;
582		    if ($result->entry) {
583    		    if (count($result->entry) > 1) {
584    		        $entries = $result->entry;
585    		    } else {
586    		        $entries = array($result->entry);
587    		    }
588		    } else {
589		        // This one is tricky... If we have properties defined, we have an entity.
590		        $properties = $result->xpath('//m:properties');
591		        if ($properties) {
592		            $entries = array($result);
593		        } else {
594		            return array();
595		        }
596		    }
597
598		    // Create return value
599		    $returnValue = array();		    
600		    foreach ($entries as $entry) {
601    		    // Parse properties
602    		    $properties = $entry->xpath('.//m:properties');
603    		    $properties = $properties[0]->children('http://schemas.microsoft.com/ado/2007/08/dataservices');
604    		    
605    		    // Create entity
606    		    $entity = new $entityClass('', '');
607    		    $entity->setAzureValues((array)$properties, $this->_throwExceptionOnMissingData);
608    		    
609    		    // If we have a Zend_Service_WindowsAzure_Storage_DynamicTableEntity, make sure all property types are set
610    		    if ($entity instanceof Zend_Service_WindowsAzure_Storage_DynamicTableEntity) {
611    		        foreach ($properties as $key => $value) {  
612    		            $attributes = $value->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
613    		            $type = (string)$attributes['type'];
614    		            if ($type !== '') {
615    		            	$entity->setAzureProperty($key, (string)$value, $type);
616    		            }
617    		        }
618    		    }
619    
620    		    // Update etag
621    		    $etag      = $entry->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
622    		    $etag      = (string)$etag['etag'];
623    		    $entity->setEtag($etag);
624    		    
625    		    // Add to result
626    		    $returnValue[] = $entity;
627		    }
628
629			// More entities?
630		    if (!is_null($response->getHeader('x-ms-continuation-NextPartitionKey')) && !is_null($response->getHeader('x-ms-continuation-NextRowKey'))) {
631		        if (strpos($queryString, '$top') === false) {
632		            $returnValue = array_merge($returnValue, $this->retrieveEntities($tableName, $filter, $entityClass, $response->getHeader('x-ms-continuation-NextPartitionKey'), $response->getHeader('x-ms-continuation-NextRowKey')));
633		        }
634		    }
635		    
636		    // Return
637		    return $returnValue;
638		} else {
639			require_once 'Zend/Service/WindowsAzure/Exception.php';
640		    throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
641		}
642	}
643	
644	/**
645	 * Update entity by replacing it
646	 * 
647	 * @param string                              $tableName   Table name
648	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
649	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
650	 * @throws Zend_Service_WindowsAzure_Exception
651	 */
652	public function updateEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
653	{
654	    return $this->_changeEntity(Zend_Http_Client::PUT, $tableName, $entity, $verifyEtag);
655	}
656	
657	/**
658	 * Update entity by adding or updating properties
659	 * 
660	 * @param string                              $tableName   Table name
661	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
662	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
663	 * @param array                               $properties  Properties to merge. All properties will be used when omitted.
664	 * @throws Zend_Service_WindowsAzure_Exception
665	 */
666	public function mergeEntity($tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false, $properties = array())
667	{
668		$mergeEntity = null;
669		if (is_array($properties) && count($properties) > 0) {
670			
671			// Build a new object
672			$mergeEntity = new Zend_Service_WindowsAzure_Storage_DynamicTableEntity($entity->getPartitionKey(), $entity->getRowKey());
673			
674			// Keep only values mentioned in $properties
675			$azureValues = $entity->getAzureValues();
676			foreach ($azureValues as $key => $value) {
677				if (in_array($value->Name, $properties)) {
678					$mergeEntity->setAzureProperty($value->Name, $value->Value, $value->Type);
679				}
680			}
681		} else {
682			$mergeEntity = $entity;
683		}
684		
685		// Ensure entity timestamp matches updated timestamp 
686        $entity->setTimestamp(new DateTime());
687        
688	    return $this->_changeEntity(Zend_Http_Client::MERGE, $tableName, $mergeEntity, $verifyEtag);
689	}
690	
691	/**
692	 * Get error message from Zend_Http_Response
693	 * 
694	 * @param Zend_Http_Response $response Repsonse
695	 * @param string $alternativeError Alternative error message
696	 * @return string
697	 */
698	protected function _getErrorMessage(Zend_Http_Response $response, $alternativeError = 'Unknown error.')
699	{
700		$response = $this->_parseResponse($response);
701		if ($response && $response->message) {
702		    return (string)$response->message;
703		} else {
704		    return $alternativeError;
705		}
706	}
707	
708	/**
709	 * Update entity / merge entity
710	 * 
711	 * @param string                              $httpVerb    HTTP verb to use (PUT = update, MERGE = merge)
712	 * @param string                              $tableName   Table name
713	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity      Entity to update
714	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
715	 * @throws Zend_Service_WindowsAzure_Exception
716	 */
717	protected function _changeEntity($httpVerb = Zend_Http_Client::PUT, $tableName = '', Zend_Service_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
718	{
719		if ($tableName === '') {
720			require_once 'Zend/Service/WindowsAzure/Exception.php';
721			throw new Zend_Service_WindowsAzure_Exception('Table name is not specified.');
722		}
723		if (is_null($entity)) {	
724			require_once 'Zend/Service/WindowsAzure/Exception.php';
725			throw new Zend_Service_WindowsAzure_Exception('Entity is not specified.');
726		}
727		                     
728        // Add header information
729        $headers = array();
730        $headers['Content-Type']   = 'application/atom+xml';
731        $headers['Content-Length'] = 0;
732        if (!$verifyEtag) {
733            $headers['If-Match']       = '*';
734        } else {
735            $headers['If-Match']       = $entity->getEtag();
736        }
737
738	    // Generate request body
739		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
740                        <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">
741                          <title />
742                          <updated>{tpl:Updated}</updated>
743                          <author>
744                            <name />
745                          </author>
746                          <id />
747                          <content type="application/xml">
748                            <m:properties>
749                              {tpl:Properties}
750                            </m:properties>
751                          </content>
752                        </entry>';
753		
754		// Attempt to get timestamp from entity
755        $timestamp = $entity->getTimestamp();
756        
757        $requestBody = $this->_fillTemplate($requestBody, array(
758        	'Updated'    => $this->_convertToEdmDateTime($timestamp),
759            'Properties' => $this->_generateAzureRepresentation($entity)
760        ));
761
762        // Add header information
763        $headers = array();
764        $headers['Content-Type'] = 'application/atom+xml';
765	    if (!$verifyEtag) {
766            $headers['If-Match']       = '*';
767        } else {
768            $headers['If-Match']       = $entity->getEtag();
769        }
770        
771		// Perform request
772		$response = null;
773	    if ($this->isInBatch()) {
774		    $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
775		    return null;
776		} else {
777		    $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
778		}
779		if ($response->isSuccessful()) {
780		    // Update properties
781			$entity->setEtag($response->getHeader('Etag'));
782			$entity->setTimestamp( $this->_convertToDateTime($response->getHeader('Last-modified')) );
783
784		    return $entity;
785		} else {
786			require_once 'Zend/Service/WindowsAzure/Exception.php';
787			throw new Zend_Service_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
788		}
789	}
790	
791	/**
792	 * Generate RFC 1123 compliant date string
793	 * 
794	 * @return string
795	 */
796	protected function _rfcDate()
797	{
798	    return gmdate('D, d M Y H:i:s', time()) . ' GMT'; // RFC 1123
799	}
800	
801	/**
802	 * Fill text template with variables from key/value array
803	 * 
804	 * @param string $templateText Template text
805	 * @param array $variables Array containing key/value pairs
806	 * @return string
807	 */
808	protected function _fillTemplate($templateText, $variables = array())
809	{
810	    foreach ($variables as $key => $value) {
811	        $templateText = str_replace('{tpl:' . $key . '}', $value, $templateText);
812	    }
813	    return $templateText;
814	}
815	
816	/**
817	 * Generate Azure representation from entity (creates atompub markup from properties)
818	 * 
819	 * @param Zend_Service_WindowsAzure_Storage_TableEntity $entity
820	 * @return string
821	 */
822	protected function _generateAzureRepresentation(Zend_Service_WindowsAzure_Storage_TableEntity $entity = null)
823	{
824		// Generate Azure representation from entity
825		$azureRepresentation = array();
826		$azureValues         = $entity->getAzureValues();
827		foreach ($azureValues as $azureValue) {
828		    $value = array();
829		    $value[] = '<d:' . $azureValue->Name;
830		    if ($azureValue->Type != '') {
831		        $value[] = ' m:type="' . $azureValue->Type . '"';
832		    }
833		    if (is_null($azureValue->Value)) {
834		        $value[] = ' m:null="true"'; 
835		    }
836		    $value[] = '>';
837		    
838		    if (!is_null($azureValue->Value)) {
839		        if (strtolower($azureValue->Type) == 'edm.boolean') {
840		            $value[] = ($azureValue->Value == true ? '1' : '0');
841		        } else if (strtolower($azureValue->Type) == 'edm.datetime') {
842		        	$value[] = $this->_convertToEdmDateTime($azureValue->Value);
843		        } else {
844		            $value[] = htmlspecialchars($azureValue->Value);
845		        }
846		    }
847		    
848		    $value[] = '</d:' . $azureValue->Name . '>';
849		    $azureRepresentation[] = implode('', $value);
850		}
851
852		return implode('', $azureRepresentation);
853	}
854	
855		/**
856	 * Perform request using Zend_Http_Client channel
857	 *
858	 * @param string $path Path
859	 * @param string $queryString Query string
860	 * @param string $httpVerb HTTP verb the request will use
861	 * @param array $headers x-ms headers to add
862	 * @param boolean $forTableStorage Is the request for table storage?
863	 * @param mixed $rawData Optional RAW HTTP data to be sent over the wire
864	 * @param string $resourceType Resource type
865	 * @param string $requiredPermission Required permission
866	 * @return Zend_Http_Response
867	 */
868	protected function _performRequest(
869		$path = '/',
870		$queryString = '',
871		$httpVerb = Zend_Http_Client::GET,
872		$headers = array(),
873		$forTableStorage = false,
874		$rawData = null,
875		$resourceType = Zend_Service_WindowsAzure_Storage::RESOURCE_UNKNOWN,
876		$requiredPermission = Zend_Service_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
877	) {
878		// Add headers
879		$headers['DataServiceVersion'] = '1.0;NetFx';
880		$headers['MaxDataServiceVersion'] = '1.0;NetFx';
881
882		// Perform request
883		return parent::_performRequest(
884			$path,
885			$queryString,
886			$httpVerb,
887			$headers,
888			$forTableStorage,
889			$rawData,
890			$resourceType,
891			$requiredPermission
892		);
893	}  
894	  
895    /**
896     * Converts a string to a DateTime object. Returns false on failure.
897     * 
898     * @param string $value The string value to parse
899     * @return DateTime|boolean
900     */
901    protected function _convertToDateTime($value = '') 
902    {
903    	if ($value instanceof DateTime) {
904    		return $value;
905    	}
906    	
907    	try {
908    		if (substr($value, -1) == 'Z') {
909    			$value = substr($value, 0, strlen($value) - 1);
910    		}
911    		return new DateTime($value, new DateTimeZone('UTC'));
912    	}
913    	catch (Exception $ex) {
914    		return false;
915    	}
916    }
917    
918    /**
919     * Converts a DateTime object into an Edm.DaeTime value in UTC timezone,
920     * represented as a string.
921     * 
922     * @param DateTime $value
923     * @return string
924     */
925    protected function _convertToEdmDateTime(DateTime $value) 
926    {
927    	$cloned = clone $value;
928    	$cloned->setTimezone(new DateTimeZone('UTC'));
929    	return str_replace('+0000', 'Z', $cloned->format(DateTime::ISO8601));
930    }
931}