PageRenderTime 49ms CodeModel.GetById 19ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/branches/v2.0.0/library/Microsoft/WindowsAzure/Storage/Table.php

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