remote__data.inc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <?php
  2. /**
  3. * @class
  4. * Purpose:
  5. *
  6. * Data:
  7. * Assumptions:
  8. */
  9. class remote__data extends WebServicesField {
  10. // --------------------------------------------------------------------------
  11. // EDITABLE STATIC CONSTANTS
  12. //
  13. // The following constants SHOULD be set for each descendant class. They are
  14. // used by the static functions to provide information to Drupal about
  15. // the field and it's default widget and formatter.
  16. // --------------------------------------------------------------------------
  17. // The default label for this field.
  18. public static $default_label = 'Remote Tripal Site';
  19. // The default description for this field.
  20. public static $default_description = 'Allows for inclusion of remote data from another Tripal site.';
  21. // The default widget for this field.
  22. public static $default_widget = 'remote__data_widget';
  23. // The default formatter for this field.
  24. public static $default_formatter = 'remote__data_formatter';
  25. // The module that manages this field.
  26. public static $module = 'tripal_ws';
  27. // A list of global settings. These can be accessed within the
  28. // globalSettingsForm. When the globalSettingsForm is submitted then
  29. // Drupal will automatically change these settings for all fields.
  30. // Once instances exist for a field type then these settings cannot be
  31. // changed.
  32. public static $default_settings = [
  33. 'storage' => 'field_tripal_ws_storage',
  34. // It is expected that all fields set a 'value' in the load() function.
  35. // In many cases, the value may be an associative array of key/value pairs.
  36. // In order for Tripal to provide context for all data, the keys should
  37. // be a controlled vocabulary term (e.g. rdfs:type). Keys in the load()
  38. // function that are supported by the query() function should be
  39. // listed here.
  40. 'searchable_keys' => [],
  41. ];
  42. // Provide a list of instance specific settings. These can be access within
  43. // the instanceSettingsForm. When the instanceSettingsForm is submitted
  44. // then Drupal with automatically change these settings for the instance.
  45. // It is recommended to put settings at the instance level whenever possible.
  46. // If you override this variable in a child class be sure to replicate the
  47. // term_name, term_vocab, term_accession and term_fixed keys as these are
  48. // required for all TripalFields.
  49. public static $default_instance_settings = [
  50. // The short name for the vocabulary (e.g. schema, SO, GO, PATO, etc.).
  51. 'term_vocabulary' => 'schema',
  52. // The name of the term.
  53. 'term_name' => 'Thing',
  54. // The unique ID (i.e. accession) of the term.
  55. 'term_accession' => 'property',
  56. // Set to TRUE if the site admin is not allowed to change the term
  57. // type, otherwise the admin can change the term mapped to a field.
  58. 'term_fixed' => FALSE,
  59. // Indicates if this field should be automatically attached to display
  60. // or web services or if this field should be loaded separately. This
  61. // is convenient for speed. Fields that are slow should for loading
  62. // should have auto_attach set to FALSE so tha their values can be
  63. // attached asynchronously.
  64. 'auto_attach' => FALSE,
  65. // Settings to allow the site admin to set the remote data source info.
  66. 'data_info' => [
  67. 'query' => '',
  68. 'remote_site' => '',
  69. 'description' => '',
  70. 'rd_field_name' => '',
  71. 'site_logo' => '',
  72. ],
  73. ];
  74. // A boolean specifying that users should not be allowed to create
  75. // fields and instances of this field type through the UI. Such
  76. // fields can only be created programmatically with field_create_field()
  77. // and field_create_instance().
  78. public static $no_ui = FALSE;
  79. // A boolean specifying that the field will not contain any data. This
  80. // should exclude the field from web services or downloads. An example
  81. // could be a quick search field that appears on the page that redirects
  82. // the user but otherwise provides no data.
  83. public static $no_data = TRUE;
  84. // Holds an object describing the remote site that tihs field connects to.
  85. private $remote_site = NULL;
  86. // Set to TRUE if this field is being loaded via web services. WE don't
  87. // want remote fields loaded when a web-service call is made.
  88. private $loaded_via_ws = FALSE;
  89. public function __construct($field, $instance) {
  90. parent::__construct($field, $instance);
  91. // This field should not do anything if it is loaded via web-services.
  92. // We don't want remote content to be available in web services. There
  93. // is an if statement to not show this field in the web services but the
  94. // entity_load function doesn't know this field shouldn't be loaded so
  95. // we need to short-circuit that.
  96. if (preg_match('/web-services/', $_SERVER['REQUEST_URI'])) {
  97. $this->loaded_via_ws = TRUE;
  98. return;
  99. }
  100. // Get the site url from the tripal_sites table.
  101. if (array_key_exists('data_info', $instance['settings'])) {
  102. $site_id_ws = $instance['settings']['data_info']['remote_site'];
  103. if ($site_id_ws) {
  104. $this->remote_site = db_select('tripal_sites', 'ts')
  105. ->fields('ts')
  106. ->condition('ts.id', $site_id_ws)
  107. ->execute()
  108. ->fetchObject();
  109. }
  110. }
  111. }
  112. /**
  113. * @see WebServicesField::load()
  114. */
  115. public function load($entity) {
  116. // If this field is being loaded via web services then just return.
  117. if ($this->loaded_via_ws == TRUE) {
  118. return;
  119. }
  120. $field_name = $this->field['field_name'];
  121. $field_type = $this->field['type'];
  122. // Set some defaults for the empty record.
  123. $entity->{$field_name}['und'][0] = [
  124. 'value' => '',
  125. 'remote_entity' => NULL,
  126. 'error' => FALSE,
  127. 'warning' => FALSE,
  128. 'admin_message' => '',
  129. 'query_str' => '',
  130. ];
  131. // Get the query set by the admin for this field and replace any tokens
  132. $query_str = $this->instance['settings']['data_info']['query'];
  133. $bundle = tripal_load_bundle_entity(['name' => $entity->bundle]);
  134. $query_str = tripal_replace_entity_tokens($query_str, $entity, $bundle);
  135. // Make the request.
  136. $data = $this->makeRemoteRequest($query_str);
  137. $context = [];
  138. if (is_array($data['@context'])) {
  139. $contenxt = $data['@context'];
  140. }
  141. else {
  142. $context = json_decode(file_get_contents($data['@context']), TRUE);
  143. $context = $context['@context'];
  144. }
  145. if (!$data) {
  146. $entity->{$field_name}['und'][0]['value'] = 'ERROR: there was a problem retrieving content for this field.';
  147. $entity->{$field_name}['und'][0]['admin_message'] = "The remote service returned no data.";
  148. $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
  149. $entity->{$field_name}['und'][0]['error'] = TRUE;
  150. $entity->{$field_name}['und'][0]['warning'] = FALSE;
  151. $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_str);
  152. return;
  153. }
  154. // Make sure we didn't have a problem
  155. if (array_key_exists('error', $data)) {
  156. $entity->{$field_name}['und'][0]['value'] = 'ERROR: there was a problem retrieving content for this field.';
  157. $entity->{$field_name}['und'][0]['admin_message'] = "The content is currently not available because the " .
  158. "remote service reported the following error: " . $data['error'] . ".";
  159. $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
  160. $entity->{$field_name}['und'][0]['error'] = TRUE;
  161. $entity->{$field_name}['und'][0]['warning'] = FALSE;
  162. $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_str);
  163. return;
  164. }
  165. $num_items = count($data['member']);
  166. if ($num_items == 0) {
  167. $entity->{$field_name}['und'][0]['value'] = 'Content is unavailable on the remote service.';
  168. $entity->{$field_name}['und'][0]['admin_message'] = "The query to the remote service returned an empty result set. If you " .
  169. "think this is an error, please check the query string and the remote service to verify. ";
  170. $entity->{$field_name}['und'][0]['warning'] = TRUE;
  171. $entity->{$field_name}['und'][0]['error'] = FALSE;
  172. $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
  173. $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_str);
  174. return;
  175. }
  176. // Iterate through the members returned and save those for the field.
  177. for ($i = 0; $i < $num_items; $i++) {
  178. $member = $data['member'][$i];
  179. // Get the cotent type and remote entity id
  180. $content_type = $member['@type'];
  181. $remote_entity_id = $member['@id'];
  182. $remote_entity_id = preg_replace('/^.*\/(\d+)/', '$1', $remote_entity_id);
  183. // Separate the query_field if it has subfields.
  184. $rd_field_name = $this->instance['settings']['data_info']['rd_field_name'];
  185. $subfields = explode(',', $rd_field_name);
  186. $query_field = $subfields[0];
  187. // Next get the the details about this member.
  188. $query_field_url = $context[$content_type] . '/' . $remote_entity_id . '/' . $query_field;
  189. $field_data = $this->makeRemoteRequest($query_field_url);
  190. // If we encounter any type of error, we'll reset the field and return.
  191. if ($field_data && array_key_exists('error', $field_data)) {
  192. $entity->{$field_name} = [];
  193. $entity->{$field_name}['und'][0]['value'] = 'ERROR: there was a problem retrieving secific content for this field.';
  194. $entity->{$field_name}['und'][0]['admin_message'] = "While iterating through the list of results, the " .
  195. "remote service reported the following error: " . $field_data['error'] . ". ";
  196. $entity->{$field_name}['und'][0]['remote_entity'] = NULL;
  197. $entity->{$field_name}['und'][0]['error'] = TRUE;
  198. $entity->{$field_name}['und'][0]['warning'] = FALSE;
  199. $entity->{$field_name}['und'][0]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_field_url);
  200. return;
  201. }
  202. // Set the field data as the value.
  203. $field_data_type = $field_data['@type'];
  204. $entity->{$field_name}['und'][$i]['value'] = $field_data;
  205. $entity->{$field_name}['und'][$i]['remote_entity'] = $member;
  206. $entity->{$field_name}['und'][$i]['error'] = FALSE;
  207. $entity->{$field_name}['und'][$i]['warning'] = FALSE;
  208. $entity->{$field_name}['und'][$i]['admin_message'] = '';
  209. $entity->{$field_name}['und'][$i]['query_str'] = $this->buildRemoteURL($this->remote_site, $query_field_url);;
  210. }
  211. }
  212. /**
  213. * Used to build the full URL for the query.
  214. */
  215. private function buildRemoteURL($remote_site, $query) {
  216. $path = $query;
  217. $q = '';
  218. if (preg_match('/\?/', $query)) {
  219. list($path, $q) = explode('?', $query);
  220. }
  221. if (empty($remote_site)) {
  222. tripal_report_error('tripal_ws', TRIPAL_ERROR, 'Unable to find remote_site in remote__data field while attempting to build the remote URL.');
  223. return NULL;
  224. }
  225. return tripal_build_remote_content_url($remote_site, $path, $q);
  226. }
  227. /**
  228. * Makes a request to a remote Tripal web services site.
  229. *
  230. * @param $query
  231. * The query string. This string is added to the URL for the remote
  232. * website.
  233. *
  234. * @return array on success or null if request fails.
  235. */
  236. private function makeRemoteRequest($query) {
  237. $path = $query;
  238. $q = '';
  239. if (preg_match('/\?/', $query)) {
  240. list($path, $q) = explode('?', $query);
  241. }
  242. if (empty($this->remote_site)) {
  243. tripal_report_error('tripal_ws', TRIPAL_ERROR, 'Unable to find remote_site while attempting to make the request.');
  244. return NULL;
  245. }
  246. try {
  247. $data = tripal_get_remote_content($this->remote_site->id, $path, $q);
  248. } catch (Exception $exception) {
  249. tripal_report_error('tripal_ws', TRIPAL_ERROR, $exception->getMessage());
  250. return NULL;
  251. }
  252. return $data;
  253. }
  254. /**
  255. *
  256. * @see TripalField::settingsForm()
  257. */
  258. public function instanceSettingsForm() {
  259. $element = parent::instanceSettingsForm();
  260. // Get the setting for the option for how this widget.
  261. $instance = $this->instance;
  262. $settings = '';
  263. $site_list = '';
  264. $tokens = [];
  265. // Get the form info from the bundle about to be saved.
  266. $bundle = tripal_load_bundle_entity(['name' => $instance['bundle']]);
  267. // Retrieve all available tokens.
  268. $tokens = tripal_get_entity_tokens($bundle);
  269. $element['data_info'] = [
  270. '#type' => 'fieldset',
  271. '#title' => 'Remote Data Settings',
  272. '#description' => 'These settings allow you to provide a Tripal web
  273. services query to identify content on another Tripal site and display
  274. that here within this field. You must specify the query to execute and
  275. the field to display.',
  276. '#collapsible' => TRUE,
  277. '#collapsed' => FALSE,
  278. '#prefix' => "<div id='set_titles-fieldset'>",
  279. '#suffix' => '</div>',
  280. ];
  281. // Get the site info from the tripal_sites table.
  282. $sites = db_select('tripal_sites', 's')
  283. ->fields('s')
  284. ->execute()->fetchAll();
  285. foreach ($sites as $site) {
  286. $rows[$site->id] = $site->name;
  287. }
  288. $element['data_info']['remote_site'] = [
  289. '#type' => 'select',
  290. '#title' => t('Remote Tripal Site'),
  291. '#options' => $rows,
  292. '#default_value' => $this->instance['settings']['data_info']['remote_site'],
  293. ];
  294. $element['data_info']['query'] = [
  295. '#type' => 'textarea',
  296. '#title' => 'Query to Execute',
  297. '#description' => 'Enter the query that will retreive the remote records. ',
  298. '#default_value' => $this->instance['settings']['data_info']['query'],
  299. '#rows' => 3,
  300. '#required' => TRUE,
  301. ];
  302. $element['data_info']['query_instructions'] = [
  303. '#type' => 'fieldset',
  304. '#title' => 'Query to Execute Instructions',
  305. '#description' => 'If the full URL to the remote tripal content web ' .
  306. 'service is "https://[tripal_site]/web-services/content/v0.1/". Then ' .
  307. 'this field should contain the text immediately after the ' .
  308. '"content/v0.1" portion of the URL. For information about building ' .
  309. 'web services queries see the online documentation at ' .
  310. l('The Tripal v3 User\'s Guide', 'http://tripal.info/tutorials/v3.x/web-services') .
  311. '. For example, suppose this field is attached to an Organism content ' .
  312. 'type on the local site, and you want to retrieve a field for the ' .
  313. 'same organism on a remote Tripal site. To retrieve the matching ' .
  314. 'record, you will want to query on the genus and species, since it ' .
  315. 'is unique and, you want them to match the organism for each specific ' .
  316. 'local organism page. You can use tokens to do this (see the ' .
  317. '"Available Tokens" fieldset below). For this example, the full ' .
  318. 'remote web service endpoint would be ' .
  319. '"https://[tripal_site]/web-services/content/v0.1/Organism" ' .
  320. 'and the query text should be ' .
  321. '"Organism?genus=[taxrank__genus]&species=[taxrank__species]".',
  322. '#collapsible' => TRUE,
  323. '#collapsed' => TRUE,
  324. ];
  325. $element['data_info']['query_instructions']['instructions'] = [
  326. '#type' => 'markup',
  327. '#markup' => '',
  328. ];
  329. $element['data_info']['token_display']['tokens'] = [
  330. '#type' => 'hidden',
  331. '#value' => serialize($tokens),
  332. ];
  333. $element['data_info']['token_display'] = [
  334. '#type' => 'fieldset',
  335. '#title' => 'Available Tokens',
  336. '#description' => 'Copy the token and paste it into the "Query" text field above.',
  337. '#collapsible' => TRUE,
  338. '#collapsed' => TRUE,
  339. ];
  340. $element['data_info']['token_display']['content'] = [
  341. '#type' => 'item',
  342. '#markup' => theme_token_list($tokens),
  343. ];
  344. $element['data_info']['rd_field_name'] = [
  345. '#type' => 'textfield',
  346. '#title' => 'Field to Display',
  347. '#description' => 'Enter the key from the results returned by the "Query to Execute" that should be displayed. See the example below for more details.',
  348. '#default_value' => $this->instance['settings']['data_info']['rd_field_name'],
  349. '#required' => TRUE,
  350. ];
  351. $element['data_info']['rd_field_name_instructions'] = [
  352. '#type' => 'fieldset',
  353. '#title' => 'Field to Display Instructions',
  354. '#description' => '',
  355. '#collapsible' => TRUE,
  356. '#collapsed' => TRUE,
  357. ];
  358. $element['data_info']['rd_field_name_instructions']['insructions'] = [
  359. '#type' => 'markup',
  360. '#markup' => 'The query from a Tripal web service response is always
  361. in JSON array arranged in key/value pairs. The key is the name of a
  362. controlled vocabulary term.
  363. <br><br>Suppose you want to query details about an organism.
  364. Consider the following JSON result
  365. <pre style="height: 200px; overflow: auto;">
  366. "@type": "organism",
  367. "label": "Anopheles gambiae",
  368. "ItemPage": "http://demo.tripal.info/3.x/bio_data/642",
  369. "type": "Organism",
  370. "abbreviation": "A.gambiae",
  371. "genus": "Anopheles",
  372. "species": "gambiae",
  373. "common_name": "mosquito",
  374. "database_cross_reference": "http://demo.tripal.info/3.x/web-services/content/v0.1/Organism/642/database+cross+reference",
  375. "equivalent_name": "Anopheles gambiae sensu stricto",
  376. "division": "Invertebrates",
  377. "mitochondrial_genetic_code_name": "Invertebrate Mitochondrial",
  378. "synonym": "Anopheles gambiae S",
  379. "genetic_code": "1",
  380. "lineage": "cellular organisms; Eukaryota; Opisthokonta; Metazoa; Eumetazoa; Bilateria; Protostomia; Ecdysozoa; Panarthropoda; Arthropoda; Mandibulata; Pancrustacea; Hexapoda; Insecta; Dicondylia; Pterygota; Neoptera; Holometabola; Diptera; Nematocera; Culicomorpha; Culicoidea; Culicidae; Anophelinae; Anopheles; Cellia; Pyretophorus; gambiae species complex",
  381. "genetic_code_name": "Standard",
  382. "genbank_common_name": "African malaria mosquito"
  383. </pre>
  384. To display the "common_name" from the JSON above you would enter the word
  385. "common_name" in the Field to Display textbox.
  386. ',
  387. ];
  388. $element['data_info']['description'] = [
  389. '#type' => 'textarea',
  390. '#title' => 'Description',
  391. '#description' => 'Describe the data being pulled in.',
  392. '#default_value' => $this->instance['settings']['data_info']['description'],
  393. '#rows' => 1,
  394. ];
  395. $fid = $this->instance['settings']['data_info']['site_logo'] ? $this->instance['settings']['data_info']['site_logo'] : NULL;
  396. $file = NULL;
  397. if ($fid) {
  398. $file = file_load($fid);
  399. }
  400. $element['data_info']['site_logo'] = [
  401. '#title' => 'Remote Site Logo',
  402. '#description' => t('When data is taken from a remote site and shown to the user,
  403. the site from which the data was retrieved is indicated. If you would like to
  404. include the logo for the remote site, please upload an image here.'),
  405. '#type' => 'managed_file',
  406. '#default_value' => $file ? $file->fid : NULL,
  407. '#theme' => 'image_widget',
  408. '#attached' => [
  409. 'css' => [
  410. 'image-preview' => drupal_get_path('module', 'image') . '/image.css',
  411. ],
  412. ],
  413. 'preview' => [
  414. '#markup' => theme('image_style', [
  415. 'style_name' => 'thumbnail',
  416. 'path' => $file ? $file->uri : '',
  417. ]),
  418. ],
  419. ];
  420. return $element;
  421. }
  422. /**
  423. * @see TripalField::instanceSettingsFormValidate()
  424. */
  425. public function instanceSettingsFormValidate($form, &$form_state) {
  426. $site_logo = $form_state['values']['instance']['settings']['data_info']['site_logo'];
  427. // If we have a site logo then add usage information.
  428. if ($site_logo) {
  429. $file = file_load($site_logo);
  430. $file_usage = file_usage_list($file);
  431. if (!array_key_exists('tripal_ws', $file_usage)) {
  432. file_usage_add($file, 'tripal_ws', 'site-logo', 1);
  433. }
  434. }
  435. }
  436. }