Stripping CVS keywords
[project/wbapi.git] / wbapi.request.inc
1 <?php
2
3 /**
4 * @file
5 *
6 * Request objects for the wbapi
7 */
8
9 /**
10 * Base request object
11 */
12 class wbapiRequest {
13
14 // API version, currently unused and for reference only.
15 public $version = '2';
16
17 /**
18 * $url holds the end point of the request, those methods that
19 * alter the path of the end point should change this property.
20 */
21 public $url;
22
23 /**
24 * $language holds the language for the request.
25 */
26 public $language;
27
28 /**
29 * $params is an array of arguments that will be added to the request. Items
30 * added to this array should *not* be urlencoded, encoded will be handed by
31 * the request() method.
32 */
33 public $params = array();
34
35 /**
36 * Factory method used to create Request objects.
37 *
38 * @param $type
39 * The key of the request type.
40 * @param $lang
41 * Language code; en, es, fr, ar, etc. Optional, defaults to current
42 * language.
43 */
44 static function factory($type, $lang = null) {
45 switch ($type) {
46 case 'languages':
47 return new wbapiRequestLanguages($lang);
48 case 'countries':
49 return new wbapiRequestCountries($lang);
50 case 'regions':
51 return new wbapiRequestRegions($lang);
52 case 'adminRegions':
53 return new wbapiRequestAdminRegions($lang);
54 case 'topics':
55 return new wbapiRequestTopics($lang);
56 case 'sources':
57 return new wbapiRequestSources($lang);
58 case 'incomeLevels':
59 return new wbapiRequestIncomeLevels($lang);
60 case 'lendingTypes':
61 return new wbapiRequestLendingTypes($lang);
62 case 'indicators':
63 return new wbapiRequestIndicators($lang);
64 case 'data':
65 return new wbapiRequestData($lang);
66 }
67 }
68
69 /**
70 * Constructor for the request, need to be invoke by extending classes.
71 */
72 function __construct($lang) {
73 global $language;
74 $this->url = 'http://' . variable_get('wbapi_url', 'open.worldbank.org');
75
76 if (!empty($lang)) {
77 $this->language = $lang;
78 $this->url .= '/'. $lang;
79 }
80 elseif (!empty($language->language)) {
81 $this->language = $language->language;
82 $this->url .= '/'. $language->language;
83 }
84 }
85
86
87 /**
88 * Add a filter to the request. Classes that extend this one should declare
89 * what $keys are accepted.
90 *
91 * @param $key
92 * The filter to set
93 * @param $value
94 * The value to set for the $key.
95 */
96 public function setFilter($key, $value) {
97 $this->params[$key] = $value;
98 return $this;
99 }
100
101 /**
102 * Manage and make the http requests. This is the main public entry point.
103 *
104 * @return an array of items or FALSE if error.
105 */
106 public function request() {
107 // As we only support JSON currently, we bail if we cannnot parse it.
108 if (!function_exists('json_decode')) {
109 watchdog('error', 'Function `json_decode` is not available.');
110 return FALSE;
111 }
112
113 // `countries` method content-length is 21178 w/xml vs 13893 w/json. So we
114 // use json if we can.
115 $this->params['format'] = 'json';
116
117 // The default `per_page` value is way to small, we raise it from 50 to 1K.
118 $this->params['per_page'] = '1000';
119
120 $url = $this->url .'?'. $this->query_string();
121
122 if ($cache = $this->cache_get($url)) {
123 $data = $cache->data;
124 wbapi_log($url, -1);
125 }
126 else {
127 $ts = microtime(true);
128 $data = $this->_request($url);
129 $this->filter_result($data);
130 $t = microtime(true) - $ts;
131 $this->cache_set($url, $data);
132 wbapi_log($url, $t);
133 }
134 return $data;
135 }
136
137 /**
138 * Generate a query string based on object parameters
139 */
140 protected function query_string() {
141 if (!empty($this->params)) {
142 $params = array();
143 foreach ($this->params as $k => $v) {
144 if ($k == 'date') {
145 // Date is a special case, contains ":" which shouldn't be urlencoded
146 list($start, $end) = explode(':', $v);
147 if (is_numeric($start) && is_numeric($end)) {
148 $params[] = "$k=$start:$end";
149 }
150 }
151 else {
152 $params[] = $k . '=' . urlencode($v);
153 }
154 }
155 return implode('&', $params);
156 }
157 }
158
159 /**
160 * Make the actual HTTP request and parse output
161 *
162 * TODO support XML also.
163 */
164 protected function _request($url, $l = 1) {
165 // Limit to ten levels of recursion.
166 if ($l > 10) {
167 watchdog('warning', "World Bank API recusion limit reached.");
168 return array();
169 }
170 $l++;
171
172 $response = drupal_http_request($url);
173
174 if ($response->code == '200') {
175 $data = json_decode($response->data);
176 if (is_array($data)) {
177 list($head, $values) = $data;
178 // A full reponse may be spread over several pages, so if there are
179 // additional pages we retrieve them.
180 if (($head->page < $head->pages) && ($head->per_page < $head->total)) {
181 $this->params['page'] = $head->page + 1;
182 $url = $this->url .'?'. $this->query_string();
183 if ($recurse = $this->_request($url, $l)) {
184 $values = array_merge($values, $recurse);
185 }
186 }
187 return $values;
188 }
189 else {
190 watchdog('error', "Didn't receive valid API response (invalid JSON).");
191 }
192 }
193 else {
194 watchdog('error', 'HTTP error !code received', array('!code' => $response->code));
195 }
196 return FALSE;
197 }
198
199 /**
200 * Populate the cache. Wrapper around Drupal's cache_get()
201 *
202 * @param $url
203 * The API url that would be used.
204 * @param $reset
205 * Set to TRUE to force a retrieval from the database.
206 */
207 protected function cache_get($url, $reset = FALSE) {
208 static $items = array();
209
210 $cid = $this->cache_id($url);
211 if (!$items[$cid] || $reset) {
212 $items[$cid] = cache_get($cid, 'cache_wbapi');
213 // Don't return temporary items more that 5 minutes old.
214 if ($items[$cid]->expire === CACHE_TEMPORARY && $items[$cid]->created > (time() + 300)) {
215 return FALSE;
216 }
217 }
218 return $items[$cid];
219 }
220
221 /**
222 * Retrieve the cache. Wrapper around Drupal's cache_set()
223 */
224 protected function cache_set($url, $data) {
225 if ($data === FALSE) {
226 // If we don't get a response we set a temporary cache to prevent hitting
227 // the API frequently for no reason.
228 cache_set($this->cache_id($url), FALSE, 'cache_wbapi', CACHE_TEMPORARY);
229 }
230 else {
231 cache_set($this->cache_id($url), $data, 'cache_wbapi');
232 }
233 }
234
235 /**
236 * Helper function to generate a cache id based on the class name and
237 * hash of the url
238 */
239 protected function cache_id($url) {
240 return get_class($this) .':'. md5($url);
241 }
242
243 /**
244 * Filters result.
245 */
246 protected function filter_result(&$result) {
247 }
248
249 /**
250 * Maps internal codes to WB API codes.
251 */
252 protected function map_code($code) {
253 switch ($code) {
254 case 'regions':
255 $codes = array();
256 foreach (wbapiRequest::factory('regions')->request() as $r) {
257 if ($r->id != 'NA') {
258 $codes[] = $r->id;
259 }
260 }
261 return implode(';', $codes);
262 case 'incomes':
263 case 'countries':
264 return 'all';
265 }
266 return $code;
267 }
268
269 /**
270 * Return country blacklist as an array.
271 */
272 protected function country_blacklist() {
273 static $blacklist;
274 if (!is_array($blacklist)) {
275 if (!$blacklist = explode(',', variable_get('wbapi_blacklist_countries', WBAPI_BLACKLIST_COUNTRIES_DEFAULT))) {
276 $blacklist = array();
277 }
278 }
279 return $blacklist;
280 }
281
282 /**
283 * Determine whether given id is a country id. Invoked from filter_result().
284 *
285 * Does not return countries that are blacklisted.
286 *
287 * Unfortunately there is no good way of determining which entity is a country
288 * and which is not. This fact is not even properly recorded in the database
289 * powering the WB API. We go for a short cut here.
290 *
291 * @todo Push filtering by country back into WB API.
292 */
293 protected function is_country($id) {
294 if (in_array($id, array('1W', '4E', '7E', '8S', 'ZF'))) {
295 return FALSE;
296 }
297 if (!$this->is_allowed_country($id)) {
298 return FALSE;
299 }
300 if (stripos($id, 'x') === 0) {
301 return FALSE;
302 }
303 return TRUE;
304 }
305
306 /**
307 * Determine whether given country id is not blacklisted.
308 *
309 * @todo: Push blacklist back into WB API.
310 */
311 protected function is_allowed_country($id) {
312 if (in_array($id, $this->country_blacklist())) {
313 return FALSE;
314 }
315 return TRUE;
316 }
317
318 /**
319 * Determines whether a given id is an income level id.
320 *
321 * wbapiRequestIncomeLevels returns three letter codes, can't be used here.
322 *
323 * @todo Push filtering by income level back into WB API.
324 */
325 protected function is_income_level($id) {
326 if (in_array($id, array('XD', 'XL', 'XM', 'XN', 'XO', 'XQ', 'XR', 'XS'))) {
327 return TRUE;
328 }
329 return FALSE;
330 }
331
332 }
333
334 /**
335 * Make request against the Countries method.
336 *
337 * This class only exposes a small subset of the full functionality. Here we
338 * allow only querying for the full list of countries and details about a
339 * single country.
340 *
341 * This class extends the normal setFilter() parameters:
342 * - "code" : an country or region id to lookup. The keywords 'countries',
343 * 'incomes' are also supported. TODO add 'regions'
344 * - "regions" : a regions to filter countries by
345 */
346 class wbapiRequestCountries extends wbapiRequest {
347 /**
348 * Request a the list of countries or metadata on single country.
349 */
350 function request() {
351 $this->url .= '/countries';
352 $this->code = $this->params['code'];
353 if (!empty($this->params['code']) &&
354 $this->params['code'] != 'incomes' &&
355 $this->params['code'] != 'countries') {
356 $this->url .= '/'. $this->params['code'];
357 unset($this->params['code']);
358 }
359 $result = parent::request();
360 return $result;
361 }
362
363 /**
364 * Filters result depending on requested code.
365 *
366 * Pass-by-reference $result to void copying very large arrays.
367 */
368 protected function filter_result(&$result) {
369 if ($result && isset($this->code)) {
370 switch ($this->code) {
371 case 'incomes':
372 $method = 'is_income_level';
373 break;
374 case 'countries':
375 $method = 'is_country';
376 break;
377 default:
378 $method = 'is_allowed_country';
379 break;
380 }
381 foreach ($result as $k => $r) {
382 if (isset($r->iso2Code) && !call_user_method($method, $this, $r->iso2Code)) {
383 unset($result[$k]);
384 }
385 }
386 if (!in_array($this->code, array('countries', 'incomes'))) {
387 $code = strtoupper($this->code);
388 foreach($result as $v) {
389 if ($v->iso2Code == $code || $v->id == $code) {
390 $result = array($v);
391 return;
392 }
393 }
394 }
395 }
396 }
397 }
398
399 /**
400 * Request list of regions.
401 *
402 * There is no direct way of retrieving this information from the API currently
403 * so we need to extract it from the country list.
404 *
405 * This class extends the normal setFilter() parameters:
406 *
407 * - "code" : a region to lookup.
408 */
409 class wbapiRequestRegions extends wbapiRequest {
410
411 /**
412 * Complete override of the parent request method.
413 */
414 function request() {
415 if (!empty($this->params['code'])) {
416 $code = strtoupper($this->params['code']);
417 unset($this->params['code']);
418 }
419
420 $url = $this->url .'/regions?'. $this->query_string();
421
422 if ($cache = $this->cache_get($url)) {
423 $data = $cache->data;
424 }
425 else {
426 $countries = wbapiRequest::factory('countries', $this->language)->request();
427 $data = array();
428 if ($countries) {
429 foreach ($countries as $v) {
430 if ($v->region->id == 'NA') {
431 // Collect aggregates
432 $aggregates[$v->id] = $v;
433 }
434 elseif (!isset($data[$v->region->id]) &&
435 $v->region->id != 'NWB' // Hide non-world-bank countries
436 ) {
437 $data[$v->region->id] = $v->region;
438 }
439 }
440
441 // Populate iso2Codes
442 foreach ($data as $k => $v) {
443 $data[$k]->iso2Code = $aggregates[$v->id]->iso2Code;
444 }
445 }
446 $this->cache_set($url, $data);
447 }
448
449 if (isset($code)) {
450 foreach($data as $v) {
451 if ($v->id == $code || $v->iso2Code == $code) {
452 return $v;
453 }
454 }
455 return FALSE;
456 }
457 return $data;
458 }
459 }
460
461 /**
462 * Request list of admin regions.
463 *
464 * There is no direct way of retrieving this information from the API currently
465 * so we need to extract it from the country list.
466 *
467 * This class extends the normal setFilter() parameters:
468 *
469 * - "code" : a region to lookup.
470 */
471 class wbapiRequestAdminRegions extends wbapiRequest {
472
473 /**
474 * Complete override of the parent request method.
475 */
476 function request() {
477 if (!empty($this->params['code'])) {
478 $code = strtoupper($this->params['code']);
479 unset($this->params['code']);
480 }
481
482 $url = $this->url .'/adminRegions?'. $this->query_string();
483
484 if ($cache = $this->cache_get($url)) {
485 $data = $cache->data;
486 }
487 else {
488 $countries = wbapiRequest::factory('countries', $this->language)->request();
489 $data = array();
490 if ($countries) {
491 foreach ($countries as $v) {
492 // @todo: revise this. We are using region id's iso2code's here.
493 if ($v->region->id == 'NA') {
494 // Collect aggregates
495 $aggregates[$v->id] = $v;
496 }
497 elseif ($v->adminregion->id && !isset($data[$v->adminregion->id]) && $v->region->id != 'NWB') {
498 $data[$v->adminregion->id] = $v->adminregion;
499 }
500 }
501
502 // Populate iso2Codes @todo: revise, we are using region id's iso2codes.
503 foreach ($data as $k => $v) {
504 if (isset($data[$k])) {
505 $data[$k]->iso2Code = $aggregates[$v->id]->iso2Code;
506 }
507 }
508 }
509 $this->cache_set($url, $data);
510 }
511
512 if (isset($code)) {
513 foreach($data as $v) {
514 if ($v->id == $code || $v->iso2Code == $code) {
515 return $v;
516 }
517 }
518 return FALSE;
519 }
520 return $data;
521 }
522 }
523
524 /**
525 * Request topics or a single topic
526 *
527 * This class extends the normal setFilter() parameters:
528 *
529 * - "id" : a topic id to lookup.
530 */
531 class wbapiRequestTopics extends wbapiRequest {
532 /**
533 * Request a the list of topics, or indicators within a single topic.
534 *
535 * @param $id
536 *
537 */
538 function request() {
539 $this->url .= '/topics';
540 $data = parent::request();
541 if (!empty($data) && isset($this->params['id'])) {
542 foreach ($data as $topic) {
543 if ($topic->id == $this->params['id']) {
544 return array($topic);
545 }
546 }
547 return FALSE;
548 }
549 return $data;
550 }
551 }
552
553 /**
554 * Retrieve a list of items from the API, and return the entire list or, if
555 * requested information about a single item. Meant to be extended by other
556 * classes which target API methods that return simple lists.
557 *
558 * This class extends the normal setFilter() parameters:
559 *
560 * - "id" : an id to lookup.
561 */
562 abstract class wbapiRequestList extends wbapiRequest {
563 /**
564 * Return the entire list or, if requested information about a single item.
565 *
566 * @param $id
567 * The API id of the source.
568 */
569 function request() {
570 if (!empty($this->params['id'])) {
571 $id = $this->params['id'];
572 if (!is_numeric($id)) {
573 $id = strtoupper($id);
574 }
575 unset($this->params['id']);
576 }
577
578 $items = parent::request();
579
580 if (isset($id)) {
581 foreach($items as $v) {
582 if ($v->id == $id) {
583 return $v;
584 }
585 }
586 return FALSE;
587 }
588 return $items;
589 }
590 }
591
592 /**
593 * Retrieve the list of sources, and return the entire list or, if requested
594 * information about a single source.
595 */
596 class wbapiRequestSources extends wbapiRequestList {
597 // Override request method to target the right path.
598 function request() {
599 $this->url .= '/sources';
600 return parent::request();
601 }
602 }
603
604 /**
605 * Retrieve the list of Income Levels, and return the entire list or, if requested
606 * information about a single level.
607 */
608 class wbapiRequestIncomeLevels extends wbapiRequestList {
609 // Override request method to target the right path.
610 // @todo Unify filtering across all request() methods.
611 function request() {
612 $blacklist = array('NA', 'NWB');
613 if (isset($this->params['id']) && in_array($this->params['id'], $blacklist)) {
614 return FALSE;
615 }
616
617 // Remove code parameter to take better advantage of caching.
618 if (!empty($this->params['id'])) {
619 $id = strtoupper($this->params['id']);
620 unset($this->params['id']);
621 }
622
623 $url = $this->url .'/incomeLevels?'. $this->query_string();
624
625 if (FALSE) { // $cache = $this->cache_get($url)) {
626 $data = $cache->data;
627 }
628 else {
629 $countries = wbapiRequest::factory('countries', $this->language)->request();
630 $data = array();
631 if ($countries) {
632 foreach ($countries as $v) {
633 if ($v->incomeLevel->id == 'NA') {
634 // Collect aggregates
635 $aggregates[$v->id] = $v;
636 }
637 elseif ($v->incomeLevel->id &&
638 !isset($data[$v->incomeLevel->id]) &&
639 !in_array($v->incomeLevel->id, $blacklist)
640 ) {
641 $data[$v->incomeLevel->id] = $v->incomeLevel;
642 }
643 }
644
645 // Populate iso2Codes.
646 foreach ($data as $k => $v) {
647 $data[$k]->iso2Code = $aggregates[$v->id]->iso2Code;
648 }
649 }
650 $this->cache_set($url, $data);
651 }
652
653 if (isset($id)) {
654 foreach($data as $v) {
655 if ($v->id == $id || $v->iso2Code == $id) {
656 return $v;
657 }
658 }
659 return FALSE;
660 }
661 return $data;
662 }
663 }
664
665 /**
666 * Retrieve the list of Lending Types, and return the entire list or, if requested
667 * information about a single type.
668 */
669 class wbapiRequestLendingTypes extends wbapiRequestList {
670 // Override request method to target the right path.
671 function request() {
672 $this->url .= '/lendingTypes';
673 return parent::request($id);
674 }
675 }
676
677 /**
678 * Retrieve the full indicator listing or, if requested, information about a
679 * single one.
680 *
681 * This class extends the normal setFilter() parameters:
682 *
683 * - "indicator" : an indicator to lookup.
684 * - "topic" : a topic ID by which to filter indicators.
685 */
686 class wbapiRequestIndicators extends wbapiRequest {
687 /**
688 * Reterive indicator list of information about a single indicator from the
689 * API.
690 */
691 function request() {
692 if (!empty($this->params['topic'])) {
693 $this->url .= '/topics/' . $this->params['topic'] .'/indicators' ;
694 }
695 else if (empty($this->params['indicator'])) {
696 $this->url .= '/indicators';
697 }
698 else {
699 $this->url .= '/indicators['. $this->params['indicator'] .']';
700 }
701 unset($this->params['indicator']);
702 $result = parent::request();
703 return $result;
704 }
705
706 /**
707 * Filter out results that weren't requested.
708 * Most filtering operations are handled by the API directly - we implement
709 * only filtering for "uncategorized" topics (indicated by
710 * $this->param['topic'] === FALSE) here.
711 */
712 protected function filter_result(&$result) {
713 if ($this->params['topic'] === FALSE) {
714 foreach ($result as $key => $item) {
715 if (!empty($item->topics)) {
716 unset($result[$key]);
717 }
718 }
719 // Rekey.
720 $result = array_values($result);
721 }
722 }
723 }
724
725 /**
726 * Request a data series from the API
727 *
728 * This class extends the normal setFilter() parameters:
729 *
730 * - "indicator" : an id to lookup. REQUIRED.
731 * - "code" : an country or region id to lookup. The keywords 'all',
732 * 'countries', 'regions' and 'incomes' are also supported. REQUIRED.
733 * - "year" : an id to lookup.
734 * - "date" : set a date range of the form "2000:2002", this parameter is
735 * incompabitle, and takes precedence over, "year".
736 *
737 */
738 class wbapiRequestData extends wbapiRequest {
739 /**
740 * Retrive a data series from the API
741 */
742 function request() {
743 // Bail if required paramaters aren't set.
744 if (empty($this->params['indicator']) || empty($this->params['indicator'])) {
745 return FALSE;
746 }
747
748 if (
749 isset($this->params['year']) &&
750 is_numeric($this->params['year']) &&
751 !isset($this->params['date'])
752 ) {
753 $this->params['date'] = $this->params['year'] .':'. $this->params['year'];
754 unset($this->params['year']);
755 }
756
757 // Don't allow requesting all years & all countries.
758 if (in_array($this->params['code'], array('all', 'regions', 'incomes'))
759 && !isset($this->params['date'])
760 ) {
761 return FALSE;
762 }
763
764 // Build URL with code mapped from internal code to WB API code.
765 $this->code = $this->params['code'];
766 $this->url .= '/countries/'. $this->map_code($this->code) .'/indicators/'. $this->params['indicator'];
767
768 // Don't attach code and indicator to URL in parent::request().
769 unset($this->params['code']);
770 unset($this->params['indicator']);
771
772 $result = parent::request();
773 return $result;
774 }
775
776 /**
777 * Filters result depending on requested code.
778 *
779 * Pass-by-reference $result to void copying very large arrays.
780 */
781 protected function filter_result(&$result) {
782 if (!empty($this->code)) {
783 switch ($this->code) {
784 case 'incomes':
785 $method = 'is_income_level';
786 break;
787 case 'countries':
788 $method = 'is_country';
789 break;
790 default:
791 return;
792 }
793 if (is_array($result)) {
794 foreach ($result as $k => $r) {
795 if (isset($r->country->id) && !call_user_method($method, $this, $r->country->id)) {
796 unset($result[$k]);
797 }
798 }
799 }
800 }
801 }
802 }
803
804 /**
805 * Fake a missing API method to get available languages.
806 */
807 class wbapiRequestLanguages {
808 // Main function, get the languages.
809 public function request() {
810 $langs = array(
811 'en' => 'English',
812 'es' => 'Español',
813 'fr' => 'Français',
814 'ar' => 'العربية',
815 );
816 $items = array();
817 foreach ($langs as $k => $v) {
818 $i = new stdClass();
819 $i->id = $k;
820 $i->value = $v;
821 $items[] = $i;
822 }
823 return $items;
824 }
825 }