PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/transport/Http.php

https://bitbucket.org/intel352/riiak
PHP | 694 lines | 287 code | 95 blank | 312 comment | 55 complexity | 7868af3acbbac800a52c4c604aa2a46e MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. namespace riiak\transport;
  3. use \CJSON,
  4. \Exception,
  5. \Yii,
  6. \CLogger;
  7. /**
  8. * The Http object allows you to perform all Riak operation
  9. * through HTTP protocol
  10. *
  11. * @package riiak.transport
  12. *
  13. * @abstract
  14. *
  15. * @method array get () get(string $url, array $requestHeaders = array(), string $content = '') Alias for processRequest('GET', ...)
  16. * @method array post () post(string $url, array $requestHeaders = array(), string $content = '') Alias for processRequest('POST', ...)
  17. * @method array put () put(string $url, array $requestHeaders = array(), string $content = '') Alias for processRequest('PUT', ...)
  18. * @method array delete() delete(string $url, array $requestHeaders = array(), string $content = '') Alias for processRequest('DELETE', ...)
  19. */
  20. abstract class Http extends \riiak\Transport {
  21. /**
  22. * @var http\Status
  23. */
  24. public $status;
  25. public function __call($name, $parameters) {
  26. /**
  27. * Adding magic support for transport->get|post|put|delete
  28. */
  29. switch ($name) {
  30. case 'get':
  31. case 'post':
  32. case 'put':
  33. case 'delete':
  34. /**
  35. * Process request.
  36. */
  37. array_unshift($parameters, strtoupper($name));
  38. return call_user_func_array(array($this, 'processRequest'), $parameters);
  39. break;
  40. }
  41. return parent::__call($name, $parameters);
  42. }
  43. public function listBuckets() {
  44. /**
  45. * Construct URL
  46. */
  47. $url = $this->buildBucketPath(null, null, array('buckets' => 'true'));
  48. Yii::trace('Running listBuckets request', 'ext.riiak.transport.http.listBuckets');
  49. /**
  50. * Run the request
  51. */
  52. $response = $this->processRequest('GET', $url);
  53. $this->validateResponse($response, 'listBuckets');
  54. /**
  55. * Return array of bucket names
  56. */
  57. $body = (array)CJSON::decode($response['body']);
  58. if (isset($body['buckets']) && is_array($body['buckets']))
  59. return array_map('urldecode', array_unique($body['buckets']));
  60. return array();
  61. }
  62. public function listBucketKeys(\riiak\Bucket $bucket) {
  63. /**
  64. * Fetch the bucket
  65. */
  66. Yii::trace('Running listKeys request for bucket "' . $bucket->name . '"', 'ext.riiak.transport.http.listKeys');
  67. $bucketArr = $this->getBucket($bucket, array('props' => 'false', 'keys' => 'stream'));
  68. if (isset($bucketArr['keys']) && is_array($bucketArr['keys']))
  69. return array_map('urldecode', array_unique($bucketArr['keys']));
  70. return array();
  71. }
  72. public function listBucketProps(\riiak\Bucket $bucket) {
  73. /**
  74. * Fetch the bucket
  75. */
  76. Yii::trace('Running listProps request for bucket "' . $bucket->name . '"', 'ext.riiak.transport.http.listProps');
  77. $bucketArr = $this->getBucket($bucket, array('props' => 'true', 'keys' => 'false'));
  78. if (isset($bucketArr['props']) && is_array($bucketArr['props']))
  79. return array_map('urldecode', array_unique($bucketArr['props']));
  80. return array();
  81. }
  82. public function getBucket(\riiak\Bucket $bucket, array $params = array()) {
  83. /**
  84. * Construct the URL
  85. */
  86. $url = $this->buildBucketKeyPath($bucket, null, null, $params);
  87. Yii::trace('Running getBucket request for bucket "' . $bucket->name . '"', 'ext.riiak.transport.http.getBucket');
  88. /**
  89. * Run the request.
  90. */
  91. $response = $this->processRequest('GET', $url);
  92. $this->validateResponse($response, 'getBucket');
  93. /**
  94. * Return decoded bucket array
  95. */
  96. return (array)CJSON::decode($response['body']);
  97. }
  98. public function setBucket(\riiak\Bucket $bucket, array $properties) {
  99. /**
  100. * Construct the contents
  101. */
  102. $headers = array('Content-Type: application/json');
  103. $content = CJSON::encode(array('props' => $properties));
  104. /**
  105. * Construct the request URL.
  106. */
  107. $url = $this->buildBucketKeyPath($bucket);
  108. Yii::trace('Running setBucket request for bucket "' . $bucket->name . '"', 'ext.riiak.transport.http.setBucket');
  109. /**
  110. * Process request & return response
  111. */
  112. $response = $this->processRequest('PUT', $url, $headers, $content);
  113. $this->validateResponse($response, 'setBucket');
  114. return true;
  115. }
  116. public function fetchObject(\riiak\Bucket $bucket, $key, array $params = null) {
  117. /**
  118. * Construct the URL
  119. */
  120. $url = $this->buildBucketKeyPath($bucket, $key, null, $params);
  121. Yii::trace('Running fetchObject request for bucket "' . $bucket->name . '"', 'ext.riiak.transport.fetchObject');
  122. /**
  123. * Process request.
  124. */
  125. $response = $this->processRequest('GET', $url);
  126. try {
  127. $this->validateResponse($response, 'fetchObject');
  128. }catch(\Exception $e) {
  129. /**
  130. * Allow 404 for missing objects
  131. * @todo Perhaps 404 should still toss exception?
  132. */
  133. if($e->getCode()!='404')
  134. throw $e;
  135. }
  136. /**
  137. * Return response
  138. */
  139. return $response;
  140. }
  141. public function storeObject(\riiak\Object $object, array $params = array()) {
  142. /**
  143. * Construct the URL
  144. */
  145. $url = $this->buildBucketKeyPath($object->bucket, $object->key, null, $params);
  146. Yii::trace('Running storeObject request for bucket "'. $object->bucket->name .'", object with key "' . $object->key . '"', 'ext.riiak.transport.storeObject');
  147. /**
  148. * Construct the headers
  149. */
  150. $headers = array('Accept: text/plain, */*; q=0.5',
  151. 'Content-Type: ' . $object->getContentType(),
  152. 'X-Riak-ClientId: ' . $object->client->clientId);
  153. /**
  154. * Add the vclock if it exists
  155. */
  156. if (!empty($object->vclock))
  157. $headers[] = 'X-Riak-Vclock: ' . $object->vclock;
  158. /**
  159. * Add the Links
  160. */
  161. foreach ($object->links as $link)
  162. $headers[] = 'Link: ' . $link->toLinkHeader($object->client);
  163. /**
  164. * Add the auto indexes
  165. */
  166. if (is_array($object->autoIndexes) && !empty($object->autoIndexes)) {
  167. if (!is_array($object->data))
  168. throw new Exception('Auto index feature requires that "$object->data" be an array.');
  169. $collisions = array();
  170. foreach ($object->autoIndexes as $index => $fieldName) {
  171. $value = null;
  172. // look up the value
  173. if (isset($object->data[$fieldName])) {
  174. $value = $object->data[$fieldName];
  175. $headers[] = 'X-Riak-Index-' . $index . ': ' . urlencode($value);
  176. // look for value collisions with normal indexes
  177. if (isset($object->indexes[$index]))
  178. if (false !== array_search($value, $object->indexes[$index]))
  179. $collisions[$index] = $value;
  180. }
  181. }
  182. $object->meta['client-autoindex'] = count($object->autoIndexes) > 0 ? CJSON::encode($object->autoIndexes) : null;
  183. $object->meta['client-autoindexcollision'] = count($collisions) > 0 ? CJSON::encode($collisions) : null;
  184. }
  185. /**
  186. * Add the indexes
  187. */
  188. foreach ($object->indexes as $index => $values)
  189. if (is_array($values))
  190. $headers[] = 'X-Riak-Index-' . $index . ': ' . implode(', ', array_map('urlencode', $values));
  191. /**
  192. * Add the metadata
  193. */
  194. foreach ($object->meta as $metaName => $metaValue)
  195. if ($metaValue !== null)
  196. $headers[] = 'X-Riak-Meta-' . $metaName . ': ' . $metaValue;
  197. if ($object->jsonize)
  198. $content = CJSON::encode($object->data);
  199. else
  200. $content = $object->data;
  201. /**
  202. * Run the operation
  203. */
  204. if ($object->key !== null) {
  205. Yii::trace('Storing object with key "' . $object->key . '" in bucket "' . $object->bucket->name . '"', 'ext.riiak.Object');
  206. $response = $this->put($url, $headers, $content);
  207. } else {
  208. Yii::trace('Storing new object in bucket "' . $object->bucket->name . '"', 'ext.riiak.Object');
  209. $response = $this->post($url, $headers, $content);
  210. }
  211. $action = array('storeObject');
  212. if (isset($params['returnbody']) && $params['returnbody'])
  213. array_push($action, 'fetchObject');
  214. $this->validateResponse($response, $action);
  215. return $response;
  216. }
  217. public function deleteObject(\riiak\Object $object, array $params = array()) {
  218. /**
  219. * Construct the URL
  220. */
  221. $url = $this->buildBucketKeyPath($object->bucket, $object->key, null, $params);
  222. Yii::trace('Running deleteObject request for object "' . $object->key . '"', 'ext.riiak.transport.deleteObject');
  223. /**
  224. * Process request.
  225. */
  226. $response = $this->processRequest('DELETE', $url);
  227. $this->validateResponse($response, 'deleteObject');
  228. /**
  229. * Return response
  230. */
  231. return $response;
  232. }
  233. /**
  234. * @todo Handle multipart/mixed response from linkwalk
  235. */
  236. public function linkWalk(\riiak\Bucket $bucket, $key, array $links, array $params = null) {
  237. /**
  238. * Construct the URL
  239. */
  240. $url = $this->buildBucketKeyPath($bucket, $key, $links, $params);
  241. Yii::trace('Running linkWalk request for object "' . $key . '"', 'ext.riiak.transport.linkWalk');
  242. /**
  243. * Process request.
  244. */
  245. $response = $this->processRequest('GET', $url);
  246. $this->validateResponse($response, 'linkWalk');
  247. /**
  248. * Return response
  249. */
  250. return $response;
  251. }
  252. public function mapReduce() {
  253. /**
  254. * @todo Build out this function
  255. */
  256. }
  257. public function secondaryIndex() {
  258. /**
  259. * @todo Build out this function
  260. */
  261. }
  262. public function ping() {
  263. Yii::trace('Pinging Riak server', 'ext.riiak.transport.http.ping');
  264. $response = $this->processRequest('GET', $this->buildUri('/' . $this->client->pingPrefix));
  265. $this->validateResponse($response, 'ping');
  266. return ($response !== NULL) && ($response['body'] == 'OK');
  267. }
  268. public function status() {
  269. /**
  270. * @todo implement
  271. */
  272. }
  273. public function listResources() {
  274. /**
  275. * @todo implement
  276. */
  277. }
  278. /**
  279. * Get status handling class object
  280. *
  281. * @return object http\Status
  282. */
  283. public function getStatusObject() {
  284. /**
  285. * Check for existing status handling class object
  286. */
  287. if (!is_object($this->status))
  288. $this->status = new http\Status();
  289. /*
  290. * Return status class object
  291. */
  292. return $this->status;
  293. }
  294. /**
  295. * Validate Riak response using http\Status class
  296. *
  297. * @param array $response
  298. * @param string|array $action Action or array of possible actions for which any related status is valid
  299. * @throws \Exception
  300. */
  301. public function validateResponse($response, $action) {
  302. $action = (array) $action;
  303. $statusObject = $this->getStatusObject();
  304. $validated = array_filter($action, function($action)use($response, $statusObject){
  305. return $statusObject->validateStatus($response, $action);
  306. });
  307. /**
  308. * If $validated is empty, no status was valid...
  309. */
  310. if($validated == array()) {
  311. $httpCode = $response['headers']['http_code'];
  312. $httpStatus = $response['headers']['http_status'];
  313. $errorMsg = (is_array($httpStatus) ? implode(', ', $httpStatus) : $httpStatus) . ' - ';
  314. /**
  315. * Check for error definitions
  316. */
  317. $actionErrors = array_map(function($action)use($statusObject, $httpCode){
  318. $errorMsg = $action.' failed with reason: ';
  319. if (array_key_exists($httpCode, $statusObject->errorCodes[$action]))
  320. $errorMsg .= $statusObject->errorCodes[$action][$httpCode];
  321. else
  322. $errorMsg .= 'An undefined error has occurred!';
  323. return $errorMsg;
  324. }, $action);
  325. $errorMsg .= implode(' -OR- ', $actionErrors);
  326. throw new Exception($errorMsg, $httpCode);
  327. }
  328. }
  329. /**
  330. * Builds URL for Riak server communication
  331. *
  332. * @return string
  333. */
  334. public function baseUrl() {
  335. return 'http' . ($this->client->ssl ? 's' : '') . '://' . $this->client->host . ':' . $this->client->port;
  336. }
  337. /**
  338. * @param string $appendPath optional
  339. * @param array $params optional
  340. *
  341. * @return string
  342. */
  343. public function buildMapReducePath($appendPath = NULL, array $params = NULL) {
  344. $path = '/' . $this->client->mapredPrefix;
  345. /**
  346. * Return constructed URL (Riak API Path)
  347. */
  348. return $this->buildUri($path . $appendPath, $params);
  349. }
  350. /**
  351. * Given bucket, key, linkspec, params, construct and return url for searching
  352. * secondary indices
  353. *
  354. * @author Eric Stevens <estevens@taglabsinc.com>
  355. *
  356. * @param \riiak\Bucket $bucket
  357. * @param string $index Index name and type (e.g. - 'indexName_bin')
  358. * @param string|int $start Starting value or exact match if no end value
  359. * @param string|int $end optional Ending value for range search
  360. * @param array $params optional Any extra query parameters
  361. *
  362. * @return string
  363. */
  364. public function buildBucketIndexPath(\riiak\Bucket $bucket, $index, $start, $end = NULL, array $params = NULL) {
  365. $path = '/' . $this->client->indexPrefix . '/' . urlencode($index) . '/' . urlencode($start);
  366. if (!is_null($end))
  367. $path .= '/' . urlencode($end);
  368. return $this->buildBucketPath($bucket, $path, $params);
  369. }
  370. /**
  371. * Builds URL for Riak bucket/keys query
  372. *
  373. * @param \riiak\Bucket $bucket
  374. * @param string $key optional
  375. * @param array $links optional
  376. * @param array $params optional
  377. *
  378. * @return string
  379. */
  380. public function buildBucketKeyPath(\riiak\Bucket $bucket, $key = NULL, array $links = NULL, array $params = NULL) {
  381. $path = '/' . $this->client->keyPrefix;
  382. /**
  383. * Add key
  384. */
  385. if (!is_null($key)) {
  386. $path .= '/' . urlencode($key);
  387. /**
  388. * Add params for link walking
  389. * bucket, tag, keep
  390. */
  391. if (!is_null($links))
  392. foreach ($links as $el)
  393. $path .= '/' . urlencode($el[0]) . ',' . urlencode($el[1]) . ',' . $el[2];
  394. }
  395. return $this->buildBucketPath($bucket, $path, $params);
  396. }
  397. /**
  398. * Builds URL for Riak bucket query
  399. *
  400. * @param \riiak\Bucket $bucket optional
  401. * @param string $appendPath optional
  402. * @param array $params optional
  403. *
  404. * @return string
  405. */
  406. public function buildBucketPath(\riiak\Bucket $bucket = NULL, $appendPath = NULL, array $params = NULL) {
  407. /**
  408. * Build http[s]://hostname:port/buckets[/bucket[/keys[/key]]]
  409. */
  410. $path = '/' . $this->client->bucketPrefix;
  411. /**
  412. * Add bucket
  413. */
  414. if (!is_null($bucket) && $bucket instanceof \riiak\Bucket)
  415. $path .= '/' . urlencode($bucket->name);
  416. /**
  417. * Return constructed URL (Riak API Path)
  418. */
  419. return $this->buildUri($path . $appendPath, $params);
  420. }
  421. /**
  422. * Generic method for building uri
  423. *
  424. * @param string $path
  425. * @param array $params optional
  426. *
  427. * @return string
  428. */
  429. public function buildUri($path, array $params = NULL) {
  430. $path = $this->baseUrl() . $path;
  431. /**
  432. * Add query parameters
  433. */
  434. if (!is_null($params))
  435. $path .= '?' . http_build_query($params, '', '&');
  436. return $path;
  437. }
  438. /**
  439. * Returns process headers along with status code & body
  440. *
  441. * @param int $httpCode
  442. * @param string $headers
  443. * @param string $body
  444. *
  445. * @return array[headers,body]
  446. */
  447. public function processResponse($httpCode, $headers, $body) {
  448. $headers = $this->processHeaders($headers);
  449. $headers = array_merge(array('http_code' => $httpCode), array_change_key_case($headers, CASE_LOWER));
  450. return array('headers' => $headers, 'body' => $body);
  451. }
  452. /**
  453. * Parse HTTP header string into an assoc array
  454. *
  455. * @param string $headers
  456. *
  457. * @return array
  458. */
  459. public function processHeaders($headers) {
  460. $retVal = array();
  461. $fields = array_filter(explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $headers)));
  462. foreach ($fields as $field) {
  463. if (preg_match('@^HTTP/1\.1\s+(\d+\s+[\w\s]+)@', $field, $match)) {
  464. if (isset($retVal['http_status']))
  465. $retVal['http_status'] = array_merge((array)$retVal['http_status'], (array)trim($match[1]));
  466. else
  467. $retVal['http_status'] = trim($match[1]);
  468. } elseif (preg_match('/([^:]+): (.+)/m', $field, $match)) {
  469. $match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
  470. if (isset($retVal[$match[1]]))
  471. $retVal[$match[1]] = array_merge((array)$retVal[$match[1]], (array)trim($match[2]));
  472. else
  473. $retVal[$match[1]] = trim($match[2]);
  474. }
  475. }
  476. return $retVal;
  477. }
  478. /**
  479. * Check if Riak server is alive
  480. *
  481. * @return bool
  482. */
  483. public function getIsAlive() {
  484. Yii::trace('Pinging Riak server', 'ext.riiak.transport.http');
  485. $response = $this->processRequest('GET', $this->buildUri('/' . $this->client->pingPrefix));
  486. return ($response != NULL) && ($response['body'] == 'OK');
  487. }
  488. /**
  489. * Get (fetch) multiple objects
  490. *
  491. * @param array $urls
  492. * @param array $requestHeaders
  493. * @param string $content
  494. *
  495. * @return array
  496. */
  497. abstract public function multiGet(array $urls, array $requestHeaders = array(), $content = '');
  498. /**
  499. * Populates the object. Only for internal use
  500. *
  501. * @param \riiak\Object $object
  502. * @param array $response Output of transport layer processing
  503. *
  504. * @return \riiak\Object
  505. */
  506. public function populate(\riiak\Object $object, array $response = array()) {
  507. $object->clear();
  508. /**
  509. * Update the object
  510. */
  511. $object->headers = $response['headers'];
  512. $object->data = $response['body'];
  513. /**
  514. * If 404 (Not Found), then clear the object
  515. */
  516. if ($object->httpCode == 404) {
  517. $object->clear();
  518. return $object;
  519. }
  520. /**
  521. * If we are here, then the object exists
  522. */
  523. $object->exists = true;
  524. /**
  525. * Parse the link header
  526. */
  527. if (array_key_exists('link', $object->headers))
  528. $object->populateLinks($object->headers['link']);
  529. /**
  530. * If 300 (siblings), load first sibling, store the rest
  531. */
  532. if ($object->httpCode == 300) {
  533. $siblings = explode("\n", trim($object->data));
  534. array_shift($siblings); # Get rid of 'Siblings:' string.
  535. $object->siblings = $siblings;
  536. $object->exists = true;
  537. return $object;
  538. }elseif ($object->httpCode == 201) {
  539. $pathParts = explode('/', $object->headers['location']);
  540. $object->key = array_pop($pathParts);
  541. }
  542. /**
  543. * Possibly JSON decode
  544. */
  545. if (($object->httpCode == 200 || $object->httpCode == 201) && $object->jsonize)
  546. $object->data = CJSON::decode($object->data, true);
  547. return $object;
  548. }
  549. /**
  550. * Get riak configuration details.
  551. *
  552. * @return array Riak configuration details
  553. */
  554. public function getRiakConfiguration() {
  555. Yii::trace('Get riak configuration', 'ext.riiak.transport.http');
  556. /**
  557. * Get riak configuration
  558. */
  559. $response = $this->processRequest('GET', $this->buildUri('/' . $this->client->statsPrefix));
  560. return CJSON::decode($response['body']);
  561. }
  562. /**
  563. * Check riak supports multi-backend functionality or not.
  564. *
  565. * @return bool
  566. */
  567. public function getIsMultiBackendSupport() {
  568. Yii::trace('Checking Riak multibackend support', 'ext.riiak.transport.http');
  569. /**
  570. * Get riak configuration
  571. */
  572. $arrConfiguration = $this->client->getConfiguration();
  573. /**
  574. * Check riak supports multibackend or not
  575. */
  576. if ($arrConfiguration['storage_backend'] == 'riak_kv_multi_backend')
  577. return true;
  578. return false;
  579. }
  580. /**
  581. * Check riak supports secondary index or not.
  582. *
  583. * @return bool
  584. * @todo Need to add check for leveldb installtion with multi-backend support.
  585. */
  586. public function getIsSecondaryIndexSupport() {
  587. Yii::trace('Checking Secondary Indexes support', 'ext.riiak.transport.http');
  588. /**
  589. * Get riak configuration
  590. */
  591. $arrConfiguration = $this->client->getConfiguration();
  592. /**
  593. * Check riak supports leveldb or not
  594. */
  595. if ($arrConfiguration['storage_backend'] == 'riak_kv_eleveldb_backend')
  596. return true;
  597. return false;
  598. }
  599. }