PageRenderTime 79ms CodeModel.GetById 8ms app.highlight 62ms RepoModel.GetById 1ms app.codeStats 0ms

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

#
PHP | 919 lines | 508 code | 107 blank | 304 comment | 98 complexity | b11f3e97f21005a6c3e4827196e01362 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	 * Create table if it does not exist
307	 *
308	 * @param string $tableName Table name
309	 * @throws Microsoft_WindowsAzure_Exception
310	 */
311	public function createTableIfNotExists($tableName = '')
312	{
313		if (!$this->tableExists($tableName)) {
314			$this->createTable($tableName);
315		}
316	}
317	
318	/**
319	 * Delete table
320	 *
321	 * @param string $tableName Table name
322	 * @throws Microsoft_WindowsAzure_Exception
323	 */
324	public function deleteTable($tableName = '')
325	{
326		if ($tableName === '') {
327			throw new Microsoft_WindowsAzure_Exception('Table name is not specified.');
328		}
329
330        // Add header information
331        $headers = array();
332        $headers['Content-Type'] = 'application/atom+xml';
333
334		// Perform request
335		$response = $this->_performRequest('Tables(\'' . $tableName . '\')', '', Microsoft_Http_Client::DELETE, $headers, true, null);
336		if (!$response->isSuccessful()) {
337			throw new Microsoft_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
338		}
339	}
340	
341	/**
342	 * Insert entity into table
343	 * 
344	 * @param string                              $tableName   Table name
345	 * @param Microsoft_WindowsAzure_Storage_TableEntity $entity      Entity to insert
346	 * @return Microsoft_WindowsAzure_Storage_TableEntity
347	 * @throws Microsoft_WindowsAzure_Exception
348	 */
349	public function insertEntity($tableName = '', Microsoft_WindowsAzure_Storage_TableEntity $entity = null)
350	{
351		if ($tableName === '') {
352			throw new Microsoft_WindowsAzure_Exception('Table name is not specified.');
353		}
354		if (is_null($entity)) {
355			throw new Microsoft_WindowsAzure_Exception('Entity is not specified.');
356		}
357		                     
358		// Generate request body
359		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
360                        <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">
361                          <title />
362                          <updated>{tpl:Updated}</updated>
363                          <author>
364                            <name />
365                          </author>
366                          <id />
367                          <content type="application/xml">
368                            <m:properties>
369                              {tpl:Properties}
370                            </m:properties>
371                          </content>
372                        </entry>';
373		
374        $requestBody = $this->_fillTemplate($requestBody, array(
375        	'Updated'    => $this->isoDate(),
376            'Properties' => $this->_generateAzureRepresentation($entity)
377        ));
378
379        // Add header information
380        $headers = array();
381        $headers['Content-Type'] = 'application/atom+xml';
382
383		// Perform request
384	    $response = null;
385	    if ($this->isInBatch()) {
386		    $this->getCurrentBatch()->enlistOperation($tableName, '', Microsoft_Http_Client::POST, $headers, true, $requestBody);
387		    return null;
388		} else {
389		    $response = $this->_performRequest($tableName, '', Microsoft_Http_Client::POST, $headers, true, $requestBody);
390		}
391		if ($response->isSuccessful()) {
392		    // Parse result
393		    $result = $this->_parseResponse($response);
394		    
395		    $timestamp = $result->xpath('//m:properties/d:Timestamp');
396		    $timestamp = (string)$timestamp[0];
397
398		    $etag      = $result->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
399		    $etag      = (string)$etag['etag'];
400		    
401		    // Update properties
402		    $entity->setTimestamp($timestamp);
403		    $entity->setEtag($etag);
404
405		    return $entity;
406		} else {
407			throw new Microsoft_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
408		}
409	}
410	
411	/**
412	 * Delete entity from table
413	 * 
414	 * @param string                              $tableName   Table name
415	 * @param Microsoft_WindowsAzure_Storage_TableEntity $entity      Entity to delete
416	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
417	 * @throws Microsoft_WindowsAzure_Exception
418	 */
419	public function deleteEntity($tableName = '', Microsoft_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
420	{
421		if ($tableName === '') {
422			throw new Microsoft_WindowsAzure_Exception('Table name is not specified.');
423		}
424		if (is_null($entity)) {
425			throw new Microsoft_WindowsAzure_Exception('Entity is not specified.');
426		}
427		                     
428        // Add header information
429        $headers = array();
430        if (!$this->isInBatch()) {
431        	// http://social.msdn.microsoft.com/Forums/en-US/windowsazure/thread/9e255447-4dc7-458a-99d3-bdc04bdc5474/
432            $headers['Content-Type']   = 'application/atom+xml';
433        }
434        $headers['Content-Length'] = 0;
435        if (!$verifyEtag) {
436            $headers['If-Match']       = '*';
437        } else {
438            $headers['If-Match']       = $entity->getEtag();
439        }
440
441		// Perform request
442	    $response = null;
443	    if ($this->isInBatch()) {
444		    $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Microsoft_Http_Client::DELETE, $headers, true, null);
445		    return null;
446		} else {
447		    $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', Microsoft_Http_Client::DELETE, $headers, true, null);
448		}
449		if (!$response->isSuccessful()) {
450		    throw new Microsoft_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
451		}
452	}
453	
454	/**
455	 * Retrieve entity from table, by id
456	 * 
457	 * @param string $tableName    Table name
458	 * @param string $partitionKey Partition key
459	 * @param string $rowKey       Row key
460	 * @param string $entityClass  Entity class name* 
461	 * @return Microsoft_WindowsAzure_Storage_TableEntity
462	 * @throws Microsoft_WindowsAzure_Exception
463	 */
464	public function retrieveEntityById($tableName = '', $partitionKey = '', $rowKey = '', $entityClass = 'Microsoft_WindowsAzure_Storage_DynamicTableEntity')
465	{
466		if ($tableName === '') {
467			throw new Microsoft_WindowsAzure_Exception('Table name is not specified.');
468		}
469		if ($partitionKey === '') {
470			throw new Microsoft_WindowsAzure_Exception('Partition key is not specified.');
471		}
472		if ($rowKey === '') {
473			throw new Microsoft_WindowsAzure_Exception('Row key is not specified.');
474		}
475		if ($entityClass === '') {
476			throw new Microsoft_WindowsAzure_Exception('Entity class is not specified.');
477		}
478
479			
480		// Check for combined size of partition key and row key
481		// http://msdn.microsoft.com/en-us/library/dd179421.aspx
482		if (strlen($partitionKey . $rowKey) >= 256) {
483		    // Start a batch if possible
484		    if ($this->isInBatch()) {
485		        throw new Microsoft_WindowsAzure_Exception('Entity cannot be retrieved. A transaction is required to retrieve the entity, but another transaction is already active.');
486		    }
487		        
488		    $this->startBatch();
489		}
490		
491		// Fetch entities from Azure
492        $result = $this->retrieveEntities(
493            $this->select()
494                 ->from($tableName)
495                 ->wherePartitionKey($partitionKey)
496                 ->whereRowKey($rowKey),
497            '',
498            $entityClass
499        );
500        
501        // Return
502        if (count($result) == 1) {
503            return $result[0];
504        }
505        
506        return null;
507	}
508	
509	/**
510	 * Create a new Microsoft_WindowsAzure_Storage_TableEntityQuery
511	 * 
512	 * @return Microsoft_WindowsAzure_Storage_TableEntityQuery
513	 */
514	public function select()
515	{
516	    return new Microsoft_WindowsAzure_Storage_TableEntityQuery();
517	}
518	
519	/**
520	 * Retrieve entities from table
521	 * 
522	 * @param string $tableName|Microsoft_WindowsAzure_Storage_TableEntityQuery    Table name -or- Microsoft_WindowsAzure_Storage_TableEntityQuery instance
523	 * @param string $filter                                                Filter condition (not applied when $tableName is a Microsoft_WindowsAzure_Storage_TableEntityQuery instance)
524	 * @param string $entityClass                                           Entity class name
525	 * @param string $nextPartitionKey                                      Next partition key, used for listing entities when total amount of entities is > 1000.
526	 * @param string $nextRowKey                                            Next row key, used for listing entities when total amount of entities is > 1000.
527	 * @return array Array of Microsoft_WindowsAzure_Storage_TableEntity
528	 * @throws Microsoft_WindowsAzure_Exception
529	 */
530	public function retrieveEntities($tableName = '', $filter = '', $entityClass = 'Microsoft_WindowsAzure_Storage_DynamicTableEntity', $nextPartitionKey = null, $nextRowKey = null)
531	{
532		if ($tableName === '') {
533			throw new Microsoft_WindowsAzure_Exception('Table name is not specified.');
534		}
535		if ($entityClass === '') {
536			throw new Microsoft_WindowsAzure_Exception('Entity class is not specified.');
537		}
538
539		// Convenience...
540		if (class_exists($filter)) {
541		    $entityClass = $filter;
542		    $filter = '';
543		}
544			
545		// Query string
546		$queryString = '';
547
548		// Determine query
549		if (is_string($tableName)) {
550		    // Option 1: $tableName is a string
551		    
552		    // Append parentheses
553		    $tableName .= '()';
554		    
555    	    // Build query
556    	    $query = array();
557    	    
558    		// Filter?
559    		if ($filter !== '') {
560    		    $query[] = '$filter=' . Microsoft_WindowsAzure_Storage_TableEntityQuery::encodeQuery($filter);
561    		}
562    		    
563    	    // Build queryString
564    	    if (count($query) > 0)  {
565    	        $queryString = '?' . implode('&', $query);
566    	    }
567		} else if (get_class($tableName) == 'Microsoft_WindowsAzure_Storage_TableEntityQuery') {
568		    // Option 2: $tableName is a Microsoft_WindowsAzure_Storage_TableEntityQuery instance
569
570		    // Build queryString
571		    $queryString = $tableName->assembleQueryString(true);
572
573		    // Change $tableName
574		    $tableName = $tableName->assembleFrom(true);
575		} else {
576		    throw new Microsoft_WindowsAzure_Exception('Invalid argument: $tableName');
577		}
578		
579		// Add continuation querystring parameters?
580		if (!is_null($nextPartitionKey) && !is_null($nextRowKey)) {
581		    if ($queryString !== '') {
582		        $queryString .= '&';
583		    }
584		        
585		    $queryString .= '&NextPartitionKey=' . rawurlencode($nextPartitionKey) . '&NextRowKey=' . rawurlencode($nextRowKey);
586		}
587
588		// Perform request
589	    $response = null;
590	    if ($this->isInBatch() && $this->getCurrentBatch()->getOperationCount() == 0) {
591		    $this->getCurrentBatch()->enlistOperation($tableName, $queryString, Microsoft_Http_Client::GET, array(), true, null);
592		    $response = $this->getCurrentBatch()->commit();
593		    
594		    // Get inner response (multipart)
595		    $innerResponse = $response->getBody();
596		    $innerResponse = substr($innerResponse, strpos($innerResponse, 'HTTP/1.1 200 OK'));
597		    $innerResponse = substr($innerResponse, 0, strpos($innerResponse, '--batchresponse'));
598		    $response = Microsoft_Http_Response::fromString($innerResponse);
599		} else {
600		    $response = $this->_performRequest($tableName, $queryString, Microsoft_Http_Client::GET, array(), true, null);
601		}
602
603		if ($response->isSuccessful()) {
604		    // Parse result
605		    $result = $this->_parseResponse($response);
606		    if (!$result) {
607		        return array();
608		    }
609
610		    $entries = null;
611		    if ($result->entry) {
612    		    if (count($result->entry) > 1) {
613    		        $entries = $result->entry;
614    		    } else {
615    		        $entries = array($result->entry);
616    		    }
617		    } else {
618		        // This one is tricky... If we have properties defined, we have an entity.
619		        $properties = $result->xpath('//m:properties');
620		        if ($properties) {
621		            $entries = array($result);
622		        } else {
623		            return array();
624		        }
625		    }
626
627		    // Create return value
628		    $returnValue = array();		    
629		    foreach ($entries as $entry) {
630    		    // Parse properties
631    		    $properties = $entry->xpath('.//m:properties');
632    		    $properties = $properties[0]->children('http://schemas.microsoft.com/ado/2007/08/dataservices');
633    		    
634    		    // Create entity
635    		    $entity = new $entityClass('', '');
636    		    $entity->setAzureValues((array)$properties, $this->_throwExceptionOnMissingData);
637    		    
638    		    // If we have a Microsoft_WindowsAzure_Storage_DynamicTableEntity, make sure all property types are OK
639    		    if ($entity instanceof Microsoft_WindowsAzure_Storage_DynamicTableEntity) {
640    		        foreach ($properties as $key => $value) {  
641    		            $attributes = $value->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
642    		            $type = (string)$attributes['type'];
643    		            if ($type !== '') {
644    		                $entity->setAzurePropertyType($key, $type);
645    		            }
646    		        }
647    		    }
648    
649    		    // Update etag
650    		    $etag      = $entry->attributes('http://schemas.microsoft.com/ado/2007/08/dataservices/metadata');
651    		    $etag      = (string)$etag['etag'];
652    		    $entity->setEtag($etag);
653    		    
654    		    // Add to result
655    		    $returnValue[] = $entity;
656		    }
657
658			// More entities?
659		    if (!is_null($response->getHeader('x-ms-continuation-NextPartitionKey')) && !is_null($response->getHeader('x-ms-continuation-NextRowKey'))) {
660		        if (strpos($queryString, '$top') === false) {
661		            $returnValue = array_merge($returnValue, $this->retrieveEntities($tableName, $filter, $entityClass, $response->getHeader('x-ms-continuation-NextPartitionKey'), $response->getHeader('x-ms-continuation-NextRowKey')));
662		        }
663		    }
664		    
665		    // Return
666		    return $returnValue;
667		} else {
668		    throw new Microsoft_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
669		}
670	}
671	
672	/**
673	 * Update entity by replacing it
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	 * @throws Microsoft_WindowsAzure_Exception
679	 */
680	public function updateEntity($tableName = '', Microsoft_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
681	{
682	    return $this->_changeEntity(Microsoft_Http_Client::PUT, $tableName, $entity, $verifyEtag);
683	}
684	
685	/**
686	 * Update entity by adding or updating properties
687	 * 
688	 * @param string                              $tableName   Table name
689	 * @param Microsoft_WindowsAzure_Storage_TableEntity $entity      Entity to update
690	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
691	 * @param array                               $properties  Properties to merge. All properties will be used when omitted.
692	 * @throws Microsoft_WindowsAzure_Exception
693	 */
694	public function mergeEntity($tableName = '', Microsoft_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false, $properties = array())
695	{
696		$mergeEntity = null;
697		if (is_array($properties) && count($properties) > 0) {
698			// Build a new object
699			$mergeEntity = new Microsoft_WindowsAzure_Storage_DynamicTableEntity($entity->getPartitionKey(), $entity->getRowKey());
700			
701			// Keep only values mentioned in $properties
702			$azureValues = $entity->getAzureValues();
703			foreach ($azureValues as $key => $value) {
704				if (in_array($value->Name, $properties)) {
705					$mergeEntity->setAzureProperty($value->Name, $value->Value, $value->Type);
706				}
707			}
708		} else {
709			$mergeEntity = $entity;
710		}
711		
712		// Ensure entity timestamp matches updated timestamp 
713        $entity->setTimestamp($this->isoDate());
714        
715	    return $this->_changeEntity(Microsoft_Http_Client::MERGE, $tableName, $mergeEntity, $verifyEtag);
716	}
717	
718	/**
719	 * Get error message from Microsoft_Http_Response
720	 * 
721	 * @param Microsoft_Http_Response $response Repsonse
722	 * @param string $alternativeError Alternative error message
723	 * @return string
724	 */
725	protected function _getErrorMessage(Microsoft_Http_Response $response, $alternativeError = 'Unknown error.')
726	{
727		$response = $this->_parseResponse($response);
728		if ($response && $response->message) {
729		    return (string)$response->message;
730		} else {
731		    return $alternativeError;
732		}
733	}
734	
735	/**
736	 * Update entity / merge entity
737	 * 
738	 * @param string                              $httpVerb    HTTP verb to use (PUT = update, MERGE = merge)
739	 * @param string                              $tableName   Table name
740	 * @param Microsoft_WindowsAzure_Storage_TableEntity $entity      Entity to update
741	 * @param boolean                             $verifyEtag  Verify etag of the entity (used for concurrency)
742	 * @throws Microsoft_WindowsAzure_Exception
743	 */
744	protected function _changeEntity($httpVerb = Microsoft_Http_Client::PUT, $tableName = '', Microsoft_WindowsAzure_Storage_TableEntity $entity = null, $verifyEtag = false)
745	{
746		if ($tableName === '') {
747			throw new Microsoft_WindowsAzure_Exception('Table name is not specified.');
748		}
749		if (is_null($entity)) {
750			throw new Microsoft_WindowsAzure_Exception('Entity is not specified.');
751		}
752		                     
753        // Add header information
754        $headers = array();
755        $headers['Content-Type']   = 'application/atom+xml';
756        $headers['Content-Length'] = 0;
757        if (!$verifyEtag) {
758            $headers['If-Match']       = '*';
759        } else {
760            $headers['If-Match']       = $entity->getEtag();
761        }
762
763	    // Generate request body
764		$requestBody = '<?xml version="1.0" encoding="utf-8" standalone="yes"?>
765                        <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">
766                          <title />
767                          <updated>{tpl:Updated}</updated>
768                          <author>
769                            <name />
770                          </author>
771                          <id />
772                          <content type="application/xml">
773                            <m:properties>
774                              {tpl:Properties}
775                            </m:properties>
776                          </content>
777                        </entry>';
778		
779		// Attempt to get timestamp from entity
780        $timestamp = $entity->getTimestamp();
781        if ($timestamp == Microsoft_WindowsAzure_Storage_TableEntity::DEFAULT_TIMESTAMP) {
782            $timestamp = $this->isoDate();
783        }
784        
785        $requestBody = $this->_fillTemplate($requestBody, array(
786        	'Updated'    => $timestamp,
787            'Properties' => $this->_generateAzureRepresentation($entity)
788        ));
789
790        // Add header information
791        $headers = array();
792        $headers['Content-Type'] = 'application/atom+xml';
793	    if (!$verifyEtag) {
794            $headers['If-Match']       = '*';
795        } else {
796            $headers['If-Match']       = $entity->getEtag();
797        }
798        
799		// Perform request
800		$response = null;
801	    if ($this->isInBatch()) {
802		    $this->getCurrentBatch()->enlistOperation($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
803		    return null;
804		} else {
805		    $response = $this->_performRequest($tableName . '(PartitionKey=\'' . $entity->getPartitionKey() . '\', RowKey=\'' . $entity->getRowKey() . '\')', '', $httpVerb, $headers, true, $requestBody);
806		}
807		if ($response->isSuccessful()) {
808		    // Update properties
809			$entity->setEtag($response->getHeader('Etag'));
810			$entity->setTimestamp($response->getHeader('Last-modified'));
811
812		    return $entity;
813		} else {
814			throw new Microsoft_WindowsAzure_Exception($this->_getErrorMessage($response, 'Resource could not be accessed.'));
815		}
816	}
817	
818	/**
819	 * Generate RFC 1123 compliant date string
820	 * 
821	 * @return string
822	 */
823	protected function _rfcDate()
824	{
825	    return gmdate('D, d M Y H:i:s', time()) . ' GMT'; // RFC 1123
826	}
827	
828	/**
829	 * Fill text template with variables from key/value array
830	 * 
831	 * @param string $templateText Template text
832	 * @param array $variables Array containing key/value pairs
833	 * @return string
834	 */
835	protected function _fillTemplate($templateText, $variables = array())
836	{
837	    foreach ($variables as $key => $value) {
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		    $value = array();
856		    $value[] = '<d:' . $azureValue->Name;
857		    if ($azureValue->Type != '') {
858		        $value[] = ' m:type="' . $azureValue->Type . '"';
859		    }
860		    if (is_null($azureValue->Value)) {
861		        $value[] = ' m:null="true"'; 
862		    }
863		    $value[] = '>';
864		    
865		    if (!is_null($azureValue->Value)) {
866		        if (strtolower($azureValue->Type) == 'edm.boolean') {
867		            $value[] = ($azureValue->Value == true ? '1' : '0');
868		        } else {
869		            $value[] = htmlspecialchars($azureValue->Value);
870		        }
871		    }
872		    
873		    $value[] = '</d:' . $azureValue->Name . '>';
874		    $azureRepresentation[] = implode('', $value);
875		}
876
877		return implode('', $azureRepresentation);
878	}
879	
880		/**
881	 * Perform request using Microsoft_Http_Client channel
882	 *
883	 * @param string $path Path
884	 * @param string $queryString Query string
885	 * @param string $httpVerb HTTP verb the request will use
886	 * @param array $headers x-ms headers to add
887	 * @param boolean $forTableStorage Is the request for table storage?
888	 * @param mixed $rawData Optional RAW HTTP data to be sent over the wire
889	 * @param string $resourceType Resource type
890	 * @param string $requiredPermission Required permission
891	 * @return Microsoft_Http_Response
892	 */
893	protected function _performRequest(
894		$path = '/',
895		$queryString = '',
896		$httpVerb = Microsoft_Http_Client::GET,
897		$headers = array(),
898		$forTableStorage = false,
899		$rawData = null,
900		$resourceType = Microsoft_WindowsAzure_Storage::RESOURCE_UNKNOWN,
901		$requiredPermission = Microsoft_WindowsAzure_Credentials_CredentialsAbstract::PERMISSION_READ
902	) {
903		// Add headers
904		$headers['DataServiceVersion'] = '1.0;NetFx';
905		$headers['MaxDataServiceVersion'] = '1.0;NetFx';
906
907		// Perform request
908		return parent::_performRequest(
909			$path,
910			$queryString,
911			$httpVerb,
912			$headers,
913			$forTableStorage,
914			$rawData,
915			$resourceType,
916			$requiredPermission
917		);
918	}
919}