6 * Request objects for the wbapi
14 // API version, currently unused and for reference only.
15 public
$version = '2';
18 * $url holds the end point of the request, those methods that
19 * alter the path of the end point should change this property.
24 * $language holds the language for the request.
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.
33 public
$params = array();
36 * Factory method used to create Request objects.
39 * The key of the request type.
41 * Language code; en, es, fr, ar, etc. Optional, defaults to current
44 static
function factory($type, $lang = null
) {
47 return new
wbapiRequestLanguages($lang);
49 return new
wbapiRequestCountries($lang);
51 return new
wbapiRequestRegions($lang);
53 return new
wbapiRequestAdminRegions($lang);
55 return new
wbapiRequestTopics($lang);
57 return new
wbapiRequestSources($lang);
59 return new
wbapiRequestIncomeLevels($lang);
61 return new
wbapiRequestLendingTypes($lang);
63 return new
wbapiRequestIndicators($lang);
65 return new
wbapiRequestData($lang);
70 * Constructor for the request, need to be invoke by extending classes.
72 function __construct($lang) {
74 $this->url
= 'http://' .
variable_get('wbapi_url', 'open.worldbank.org');
77 $this->language
= $lang;
78 $this->url .
= '/'.
$lang;
80 elseif (!empty($language->language
)) {
81 $this->language
= $language->language
;
82 $this->url .
= '/'.
$language->language
;
88 * Add a filter to the request. Classes that extend this one should declare
89 * what $keys are accepted.
94 * The value to set for the $key.
96 public
function setFilter($key, $value) {
97 $this->params
[$key] = $value;
102 * Manage and make the http requests. This is the main public entry point.
104 * @return an array of items or FALSE if error.
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.');
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';
117 // The default `per_page` value is way to small, we raise it from 50 to 1K.
118 $this->params
['per_page'] = '1000';
120 $url = $this->url .
'?'.
$this->query_string();
122 if ($cache = $this->cache_get($url)) {
123 $data = $cache->data
;
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);
138 * Generate a query string based on object parameters
140 protected
function query_string() {
141 if (!empty($this->params
)) {
143 foreach ($this->params as
$k => $v) {
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";
152 $params[] = $k .
'=' .
urlencode($v);
155 return implode('&', $params);
160 * Make the actual HTTP request and parse output
162 * TODO support XML also.
164 protected
function _request($url, $l = 1) {
165 // Limit to ten levels of recursion.
167 watchdog('warning', "World Bank API recusion limit reached.");
172 $response = drupal_http_request($url);
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);
190 watchdog('error', "Didn't receive valid API response (invalid JSON).");
194 watchdog('error', 'HTTP error !code received', array('!code' => $response->code
));
200 * Populate the cache. Wrapper around Drupal's cache_get()
203 * The API url that would be used.
205 * Set to TRUE to force a retrieval from the database.
207 protected
function cache_get($url, $reset = FALSE
) {
208 static
$items = array();
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)) {
222 * Retrieve the cache. Wrapper around Drupal's cache_set()
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
);
231 cache_set($this->cache_id($url), $data, 'cache_wbapi');
236 * Helper function to generate a cache id based on the class name and
239 protected
function cache_id($url) {
240 return get_class($this) .
':'.
md5($url);
246 protected
function filter_result(&$result) {
250 * Maps internal codes to WB API codes.
252 protected
function map_code($code) {
256 foreach (wbapiRequest
::factory('regions')->request() as
$r) {
257 if ($r->id
!= 'NA') {
261 return implode(';', $codes);
270 * Return country blacklist as an array.
272 protected
function country_blacklist() {
274 if (!is_array($blacklist)) {
275 if (!$blacklist = explode(',', variable_get('wbapi_blacklist_countries', WBAPI_BLACKLIST_COUNTRIES_DEFAULT
))) {
276 $blacklist = array();
283 * Determine whether given id is a country id. Invoked from filter_result().
285 * Does not return countries that are blacklisted.
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.
291 * @todo Push filtering by country back into WB API.
293 protected
function is_country($id) {
294 if (in_array($id, array('1W', '4E', '7E', '8S', 'ZF'))) {
297 if (!$this->is_allowed_country($id)) {
300 if (stripos($id, 'x') === 0) {
307 * Determine whether given country id is not blacklisted.
309 * @todo: Push blacklist back into WB API.
311 protected
function is_allowed_country($id) {
312 if (in_array($id, $this->country_blacklist())) {
319 * Determines whether a given id is an income level id.
321 * wbapiRequestIncomeLevels returns three letter codes, can't be used here.
323 * @todo Push filtering by income level back into WB API.
325 protected
function is_income_level($id) {
326 if (in_array($id, array('XD', 'XL', 'XM', 'XN', 'XO', 'XQ', 'XR', 'XS'))) {
335 * Make request against the Countries method.
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
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
346 class wbapiRequestCountries
extends wbapiRequest
{
348 * Request a the list of countries or metadata on single country.
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']);
359 $result = parent
::request();
364 * Filters result depending on requested code.
366 * Pass-by-reference $result to void copying very large arrays.
368 protected
function filter_result(&$result) {
369 if ($result && isset($this->code
)) {
370 switch ($this->code
) {
372 $method = 'is_income_level';
375 $method = 'is_country';
378 $method = 'is_allowed_country';
381 foreach ($result as
$k => $r) {
382 if (isset($r->iso2Code
) && !call_user_method($method, $this, $r->iso2Code
)) {
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) {
400 * Request list of regions.
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.
405 * This class extends the normal setFilter() parameters:
407 * - "code" : a region to lookup.
409 class wbapiRequestRegions
extends wbapiRequest
{
412 * Complete override of the parent request method.
415 if (!empty($this->params
['code'])) {
416 $code = strtoupper($this->params
['code']);
417 unset($this->params
['code']);
420 $url = $this->url .
'/regions?'.
$this->query_string();
422 if ($cache = $this->cache_get($url)) {
423 $data = $cache->data
;
426 $countries = wbapiRequest
::factory('countries', $this->language
)->request();
429 foreach ($countries as
$v) {
430 if ($v->region
->id
== 'NA') {
431 // Collect aggregates
432 $aggregates[$v->id
] = $v;
434 elseif (!isset($data[$v->region
->id
]) &&
435 $v->region
->id
!= 'NWB' // Hide non-world-bank countries
437 $data[$v->region
->id
] = $v->region
;
441 // Populate iso2Codes
442 foreach ($data as
$k => $v) {
443 $data[$k]->iso2Code
= $aggregates[$v->id
]->iso2Code
;
446 $this->cache_set($url, $data);
450 foreach($data as
$v) {
451 if ($v->id
== $code || $v->iso2Code
== $code) {
462 * Request list of admin regions.
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.
467 * This class extends the normal setFilter() parameters:
469 * - "code" : a region to lookup.
471 class wbapiRequestAdminRegions
extends wbapiRequest
{
474 * Complete override of the parent request method.
477 if (!empty($this->params
['code'])) {
478 $code = strtoupper($this->params
['code']);
479 unset($this->params
['code']);
482 $url = $this->url .
'/adminRegions?'.
$this->query_string();
484 if ($cache = $this->cache_get($url)) {
485 $data = $cache->data
;
488 $countries = wbapiRequest
::factory('countries', $this->language
)->request();
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;
497 elseif ($v->adminregion
->id
&& !isset($data[$v->adminregion
->id
]) && $v->region
->id
!= 'NWB') {
498 $data[$v->adminregion
->id
] = $v->adminregion
;
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
;
509 $this->cache_set($url, $data);
513 foreach($data as
$v) {
514 if ($v->id
== $code || $v->iso2Code
== $code) {
525 * Request topics or a single topic
527 * This class extends the normal setFilter() parameters:
529 * - "id" : a topic id to lookup.
531 class wbapiRequestTopics
extends wbapiRequest
{
533 * Request a the list of topics, or indicators within a single topic.
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);
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.
558 * This class extends the normal setFilter() parameters:
560 * - "id" : an id to lookup.
562 abstract
class wbapiRequestList
extends wbapiRequest
{
564 * Return the entire list or, if requested information about a single item.
567 * The API id of the source.
570 if (!empty($this->params
['id'])) {
571 $id = $this->params
['id'];
572 if (!is_numeric($id)) {
573 $id = strtoupper($id);
575 unset($this->params
['id']);
578 $items = parent
::request();
581 foreach($items as
$v) {
593 * Retrieve the list of sources, and return the entire list or, if requested
594 * information about a single source.
596 class wbapiRequestSources
extends wbapiRequestList
{
597 // Override request method to target the right path.
599 $this->url .
= '/sources';
600 return parent
::request();
605 * Retrieve the list of Income Levels, and return the entire list or, if requested
606 * information about a single level.
608 class wbapiRequestIncomeLevels
extends wbapiRequestList
{
609 // Override request method to target the right path.
610 // @todo Unify filtering across all request() methods.
612 $blacklist = array('NA', 'NWB');
613 if (isset($this->params
['id']) && in_array($this->params
['id'], $blacklist)) {
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']);
623 $url = $this->url .
'/incomeLevels?'.
$this->query_string();
625 if (FALSE
) { // $cache = $this->cache_get($url)) {
626 $data = $cache->data
;
629 $countries = wbapiRequest
::factory('countries', $this->language
)->request();
632 foreach ($countries as
$v) {
633 if ($v->incomeLevel
->id
== 'NA') {
634 // Collect aggregates
635 $aggregates[$v->id
] = $v;
637 elseif ($v->incomeLevel
->id
&&
638 !isset($data[$v->incomeLevel
->id
]) &&
639 !in_array($v->incomeLevel
->id
, $blacklist)
641 $data[$v->incomeLevel
->id
] = $v->incomeLevel
;
645 // Populate iso2Codes.
646 foreach ($data as
$k => $v) {
647 $data[$k]->iso2Code
= $aggregates[$v->id
]->iso2Code
;
650 $this->cache_set($url, $data);
654 foreach($data as
$v) {
655 if ($v->id
== $id || $v->iso2Code
== $id) {
666 * Retrieve the list of Lending Types, and return the entire list or, if requested
667 * information about a single type.
669 class wbapiRequestLendingTypes
extends wbapiRequestList
{
670 // Override request method to target the right path.
672 $this->url .
= '/lendingTypes';
673 return parent
::request($id);
678 * Retrieve the full indicator listing or, if requested, information about a
681 * This class extends the normal setFilter() parameters:
683 * - "indicator" : an indicator to lookup.
684 * - "topic" : a topic ID by which to filter indicators.
686 class wbapiRequestIndicators
extends wbapiRequest
{
688 * Reterive indicator list of information about a single indicator from the
692 if (!empty($this->params
['topic'])) {
693 $this->url .
= '/topics/' .
$this->params
['topic'] .
'/indicators' ;
695 else if (empty($this->params
['indicator'])) {
696 $this->url .
= '/indicators';
699 $this->url .
= '/indicators['.
$this->params
['indicator'] .
']';
701 unset($this->params
['indicator']);
702 $result = parent
::request();
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.
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]);
720 $result = array_values($result);
726 * Request a data series from the API
728 * This class extends the normal setFilter() parameters:
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".
738 class wbapiRequestData
extends wbapiRequest
{
740 * Retrive a data series from the API
743 // Bail if required paramaters aren't set.
744 if (empty($this->params
['indicator']) || empty($this->params
['indicator'])) {
749 isset($this->params
['year']) &&
750 is_numeric($this->params
['year']) &&
751 !isset($this->params
['date'])
753 $this->params
['date'] = $this->params
['year'] .
':'.
$this->params
['year'];
754 unset($this->params
['year']);
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'])
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'];
768 // Don't attach code and indicator to URL in parent::request().
769 unset($this->params
['code']);
770 unset($this->params
['indicator']);
772 $result = parent
::request();
777 * Filters result depending on requested code.
779 * Pass-by-reference $result to void copying very large arrays.
781 protected
function filter_result(&$result) {
782 if (!empty($this->code
)) {
783 switch ($this->code
) {
785 $method = 'is_income_level';
788 $method = 'is_country';
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
)) {
805 * Fake a missing API method to get available languages.
807 class wbapiRequestLanguages
{
808 // Main function, get the languages.
809 public
function request() {
817 foreach ($langs as
$k => $v) {