PageRenderTime 57ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/Microsoft/WindowsAzure/Storage/Table.php

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