PageRenderTime 92ms CodeModel.GetById 14ms app.highlight 53ms RepoModel.GetById 17ms app.codeStats 0ms

/PhpSiteGenerator/PhpSiteGenerator/Microsoft/WindowsAzure/Storage/Table.php

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