PageRenderTime 79ms CodeModel.GetById 48ms app.highlight 25ms RepoModel.GetById 0ms app.codeStats 1ms

/src/frapi/library/Zend/Service/WindowsAzure/Storage/Table.php

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