PageRenderTime 175ms CodeModel.GetById 148ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/tags/storage-2009-04-14/library/Microsoft/WindowsAzure/Storage/Table.php

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