PageRenderTime 6ms CodeModel.GetById 15ms app.highlight 39ms RepoModel.GetById 1ms app.codeStats 0ms

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

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