PageRenderTime 212ms CodeModel.GetById 186ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/trunk/MoodleWebRole/azure/Microsoft/WindowsAzure/Storage/Table.php

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