PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/Service/WindowsAzure/Storage/Table.php

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