PageRenderTime 170ms CodeModel.GetById 80ms app.highlight 35ms RepoModel.GetById 45ms app.codeStats 1ms

/vendor/Microsoft/WindowsAzure/Storage/Table.php

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