OBOImporter.inc 84 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565
  1. <?php
  2. class OBOImporter extends TripalImporter {
  3. // --------------------------------------------------------------------------
  4. // EDITABLE STATIC CONSTANTS
  5. //
  6. // The following constants SHOULD be set for each descendent class. They are
  7. // used by the static functions to provide information to Drupal about
  8. // the field and it's default widget and formatter.
  9. // --------------------------------------------------------------------------
  10. /**
  11. * The name of this loader. This name will be presented to the site
  12. * user.
  13. */
  14. public static $name = 'OBO Vocabulary Loader';
  15. /**
  16. * The machine name for this loader. This name will be used to construct
  17. * the URL for the loader.
  18. */
  19. public static $machine_name = 'chado_obo_loader';
  20. /**
  21. * A brief description for this loader. This description will be
  22. * presented to the site user.
  23. */
  24. public static $description = 'Import vocabularies and terms in OBO format.';
  25. /**
  26. * An array containing the extensions of allowed file types.
  27. */
  28. public static $file_types = ['obo'];
  29. /**
  30. * Provides information to the user about the file upload. Typically this
  31. * may include a description of the file types allowed.
  32. */
  33. public static $upload_description = 'Please provide the details for importing a new OBO file. The file must have a .obo extension.';
  34. /**
  35. * The title that should appear above the upload button.
  36. */
  37. public static $upload_title = 'New OBO File';
  38. /**
  39. * If the loader should require an analysis record. To maintain provenance
  40. * we should always indicate where the data we are uploading comes from.
  41. * The method that Tripal attempts to use for this by associating upload files
  42. * with an analysis record. The analysis record provides the details for
  43. * how the file was created or obtained. Set this to FALSE if the loader
  44. * should not require an analysis when loading. if $use_analysis is set to
  45. * true then the form values will have an 'analysis_id' key in the $form_state
  46. * array on submitted forms.
  47. */
  48. public static $use_analysis = FALSE;
  49. /**
  50. * If the $use_analysis value is set above then this value indicates if the
  51. * analysis should be required.
  52. */
  53. public static $require_analysis = TRUE;
  54. /**
  55. * Text that should appear on the button at the bottom of the importer
  56. * form.
  57. */
  58. public static $button_text = 'Import OBO File';
  59. /**
  60. * Indicates the methods that the file uploader will support.
  61. */
  62. public static $methods = [
  63. // Allow the user to upload a file to the server.
  64. 'file_upload' => FALSE,
  65. // Allow the user to provide the path on the Tripal server for the file.
  66. 'file_local' => FALSE,
  67. // Allow the user to provide a remote URL for the file.
  68. 'file_remote' => FALSE,
  69. ];
  70. /**
  71. * Be default, all loaders are automaticlly added to the Admin >
  72. * Tripal > Data Loaders menu. However, if this loader should be
  73. * made available via a different menu path, then set it here. If the
  74. * value is empty then the path will be the default.
  75. */
  76. public static $menu_path = 'admin/tripal/loaders/chado_vocabs/obo_loader';
  77. public static $file_required = FALSE;
  78. /**
  79. * Keep track of vocabularies that have been added.
  80. *
  81. * @var array
  82. */
  83. private $obo_namespaces = [];
  84. /**
  85. * Holds the list of all CVs on this site. By storing them here it saves
  86. * us query time later.
  87. */
  88. private $all_cvs = [];
  89. /**
  90. * Holds the list of all DBs on this site. By storing them here it saves
  91. * us query time later.
  92. *
  93. * @var array
  94. */
  95. private $all_dbs = [];
  96. /**
  97. * When adding synonyms we need to know the cvterm_ids of the synonym types.
  98. * This array holds those.
  99. *
  100. * @var array
  101. */
  102. private $syn_types = [
  103. 'exact' => NULL,
  104. 'broad' => NULL,
  105. 'narrow' => NULL,
  106. 'related' => NULL,
  107. ];
  108. // An alternative cache to the temp_obo table.
  109. private $termStanzaCache = [
  110. 'ids' => [],
  111. 'count' => [
  112. 'Typedef' => 0,
  113. 'Term' => 0,
  114. 'Instance' => 0,
  115. ],
  116. 'types' => [
  117. 'Typedef' => [],
  118. 'Term' => [],
  119. 'Instance' => [],
  120. ],
  121. ];
  122. /**
  123. * Indicates how terms are cached. Values can be 'memory' or 'table'. If
  124. * 'memory' then the $termStanzaCache variable is used. If 'table', then the
  125. * tripal_obo_temp table is used.
  126. *
  127. * @var string
  128. */
  129. private $cache_type = 'memory';
  130. /**
  131. * The default namespace for all terms that don't have a 'namespace' in their
  132. * term stanza.
  133. *
  134. * @var string
  135. */
  136. private $default_namespace = '';
  137. /**
  138. * Holds the idspace elements from the header. These will correspond
  139. * to the accession prefixes, or short names (e.g. GO) for the terms. For
  140. * example, the EDAM vocabulary has several id spaces:
  141. * format, data, operation and topic.
  142. */
  143. private $idspaces = [];
  144. /**
  145. * The default database prefix for this ontology.
  146. *
  147. * @var string
  148. */
  149. private $default_db = '';
  150. /**
  151. * An array of used cvterm objects so that we don't have to look them
  152. * up repeatedly.
  153. */
  154. private $used_terms = [];
  155. /**
  156. * An array of base IRIs returned from the EBI OLS lookup service. We
  157. * don't want to continually query OLS for the same ontology base IRIs.
  158. */
  159. private $baseIRIs = [];
  160. /**
  161. * A flag to keep track if the user was warned about slowness when doing
  162. * EBI Lookups.
  163. *
  164. * @var string
  165. */
  166. private $ebi_warned = FALSE;
  167. /**
  168. * A flag that indicates if this ontology is just a subset of a much larger
  169. * one. Examples include the GO slims.
  170. *
  171. * @var string
  172. */
  173. private $is_subset = FALSE;
  174. /**
  175. * Sometimes an OBO can define two terms with the same name but different
  176. * IDs (e.g. GO:0001404 and GO:0007125). We need to find these and
  177. * deal with them. This array keeps track of term names as we see them for
  178. * easy lookup later.
  179. *
  180. * @var array
  181. */
  182. private $term_names = [];
  183. /**
  184. * @see TripalImporter::form()
  185. */
  186. public function form($form, &$form_state) {
  187. // get a list of db from chado for user to choose
  188. $sql = "SELECT * FROM {tripal_cv_obo} ORDER BY name";
  189. $results = db_query($sql);
  190. $obos = [];
  191. $obos[] = 'Select a Vocabulary';
  192. foreach ($results as $obo) {
  193. $obos[$obo->obo_id] = $obo->name;
  194. }
  195. $obo_id = '';
  196. if (array_key_exists('values', $form_state)) {
  197. $obo_id = array_key_exists('obo_id', $form_state['values']) ? $form_state['values']['obo_id'] : '';
  198. }
  199. $form['instructions']['info'] = [
  200. '#type' => 'item',
  201. '#markup' => t('This page allows you to load vocabularies and ontologies
  202. that are in OBO format. Once loaded, the terms from these
  203. vocabularies can be used to create content.
  204. You may use the form below to either reload a vocabulary that is already
  205. loaded (as when new updates to that vocabulary are available) or load a new
  206. vocabulary.'),
  207. ];
  208. $form['obo_existing'] = [
  209. '#type' => 'fieldset',
  210. '#title' => t('Use a Saved Ontology OBO Reference'),
  211. '#prefix' => '<span id="obo-existing-fieldset">',
  212. '#suffix' => '</span>',
  213. ];
  214. $form['obo_existing']['existing_instructions'] = [
  215. '#type' => 'item',
  216. '#markup' => t('The vocabularies listed in the select box below have been pre-populated
  217. upon installation of Tripal or have been previously loaded. Select one to edit
  218. its settings or submit for loading. You may reload any vocabulary that has
  219. already been loaded to retrieve any new updates.'),
  220. ];
  221. $form['obo_existing']['obo_id'] = [
  222. '#title' => t('Ontology OBO File Reference'),
  223. '#type' => 'select',
  224. '#options' => $obos,
  225. '#ajax' => [
  226. 'callback' => 'tripal_cv_obo_form_ajax_callback',
  227. 'wrapper' => 'obo-existing-fieldset',
  228. ],
  229. '#description' => t('Select a vocabulary to import.'),
  230. ];
  231. // If the user has selected an OBO ID then get the form elements for
  232. // updating.
  233. if ($obo_id) {
  234. $uobo_name = '';
  235. $uobo_url = '';
  236. $uobo_file = '';
  237. $vocab = db_select('tripal_cv_obo', 't')
  238. ->fields('t', ['name', 'path'])
  239. ->condition('obo_id', $obo_id)
  240. ->execute()
  241. ->fetchObject();
  242. $uobo_name = $vocab->name;
  243. if (preg_match('/^http/', $vocab->path)) {
  244. $uobo_url = $vocab->path;
  245. }
  246. else {
  247. $uobo_file = trim($vocab->path);
  248. $matches = [];
  249. if (preg_match('/\{(.*?)\}/', $uobo_file, $matches)) {
  250. $modpath = drupal_get_path('module', $matches[1]);
  251. $uobo_file = preg_replace('/\{.*?\}/', $modpath, $uobo_file);
  252. }
  253. }
  254. // We don't want the previous value to remain. We want the new default to
  255. // show up, so remove the input values
  256. unset($form_state['input']['uobo_name']);
  257. unset($form_state['input']['uobo_url']);
  258. unset($form_state['input']['uobo_file']);
  259. $form['obo_existing']['uobo_name'] = [
  260. '#type' => 'textfield',
  261. '#title' => t('Vocabulary Name'),
  262. '#description' => t('Please provide a name for this vocabulary. After upload, this name will appear in the drop down
  263. list above for use again later.'),
  264. '#default_value' => $uobo_name,
  265. ];
  266. $form['obo_existing']['uobo_url'] = [
  267. '#type' => 'textfield',
  268. '#title' => t('Remote URL'),
  269. '#description' => t('Please enter a URL for the online OBO file. The file will be downloaded and parsed.
  270. (e.g. https://raw.githubusercontent.com/oborel/obo-relations/master/ro.obo)'),
  271. '#default_value' => $uobo_url,
  272. ];
  273. $form['obo_existing']['uobo_file'] = [
  274. '#type' => 'textfield',
  275. '#title' => t('Local File'),
  276. '#description' => t('Please enter the file system path for an OBO
  277. definition file. If entering a path relative to
  278. the Drupal installation you may use a relative path that excludes the
  279. Drupal installation directory (e.g. sites/default/files/xyz.obo). Note
  280. that Drupal relative paths have no preceeding slash.
  281. Otherwise, please provide the full path on the filesystem. The path
  282. must be accessible to the web server on which this Drupal instance is running.'),
  283. '#default_value' => $uobo_file,
  284. ];
  285. $form['obo_existing']['update_obo_details'] = [
  286. '#type' => 'submit',
  287. '#value' => 'Update Ontology Details',
  288. '#name' => 'update_obo_details',
  289. ];
  290. }
  291. $form['obo_new'] = [
  292. '#type' => 'fieldset',
  293. '#title' => t('Add a New Ontology OBO Reference'),
  294. '#collapsible' => TRUE,
  295. '#collapsed' => TRUE,
  296. ];
  297. $form['obo_new']['path_instructions'] = [
  298. '#value' => t('Provide the name and path for the OBO file. If the vocabulary OBO file
  299. is stored local to the server provide a file name. If the vocabulary is stored remotely,
  300. provide a URL. Only provide a URL or a local file, not both.'),
  301. ];
  302. $form['obo_new']['obo_name'] = [
  303. '#type' => 'textfield',
  304. '#title' => t('New Vocabulary Name'),
  305. '#description' => t('Please provide a name for this vocabulary. After upload, this name will appear in the drop down
  306. list above for use again later. Additionally, if a default namespace is not provided in the OBO
  307. header this name will be used as the default_namespace.'),
  308. ];
  309. $form['obo_new']['obo_url'] = [
  310. '#type' => 'textfield',
  311. '#title' => t('Remote URL'),
  312. '#description' => t('Please enter a URL for the online OBO file. The file will be downloaded and parsed.
  313. (e.g. https://raw.githubusercontent.com/oborel/obo-relations/master/ro.obo)'),
  314. ];
  315. $form['obo_new']['obo_file'] = [
  316. '#type' => 'textfield',
  317. '#title' => t('Local File'),
  318. '#description' => t('Please enter the file system path for an OBO
  319. definition file. If entering a path relative to
  320. the Drupal installation you may use a relative path that excludes the
  321. Drupal installation directory (e.g. sites/default/files/xyz.obo). Note
  322. that Drupal relative paths have no preceeding slash.
  323. Otherwise, please provide the full path on the filesystem. The path
  324. must be accessible to the web server on which this Drupal instance is running.'),
  325. ];
  326. return $form;
  327. }
  328. /**
  329. * @see TripalImporter::formSubmit()
  330. */
  331. public function formSubmit($form, &$form_state) {
  332. $obo_id = $form_state['values']['obo_id'];
  333. $obo_name = trim($form_state['values']['obo_name']);
  334. $obo_url = trim($form_state['values']['obo_url']);
  335. $obo_file = trim($form_state['values']['obo_file']);
  336. $uobo_name = array_key_exists('uobo_name', $form_state['values']) ? trim($form_state['values']['uobo_name']) : '';
  337. $uobo_url = array_key_exists('uobo_url', $form_state['values']) ? trim($form_state['values']['uobo_url']) : '';
  338. $uobo_file = array_key_exists('uobo_file', $form_state['values']) ? trim($form_state['values']['uobo_file']) : '';
  339. // If the user requested to alter the details then do that.
  340. if ($form_state['clicked_button']['#name'] == 'update_obo_details') {
  341. $form_state['rebuild'] = TRUE;
  342. $success = db_update('tripal_cv_obo')
  343. ->fields([
  344. 'name' => $uobo_name,
  345. 'path' => $uobo_url ? $uobo_url : $uobo_file,
  346. ])
  347. ->condition('obo_id', $obo_id)
  348. ->execute();
  349. if ($success) {
  350. drupal_set_message(t("The vocabulary !vocab has been updated.", ['!vocab' => $uobo_name]));
  351. }
  352. else {
  353. drupal_set_message(t("The vocabulary !vocab could not be updated.", ['!vocab' => $uobo_name]), 'error');
  354. }
  355. }
  356. elseif (!empty($obo_name)) {
  357. $obo_id = db_insert('tripal_cv_obo')
  358. ->fields([
  359. 'name' => $obo_name,
  360. 'path' => $obo_url ? $obo_url : $obo_file,
  361. ])
  362. ->execute();
  363. // Add the obo_id to the form_state values.
  364. $form_state['values']['obo_id'] = $obo_id;
  365. if ($obo_id) {
  366. drupal_set_message(t("The vocabulary !vocab has been added.", ['!vocab' => $obo_name]));
  367. }
  368. else {
  369. $form_state['rebuild'] = TRUE;
  370. drupal_set_message(t("The vocabulary !vocab could not be added.", ['!vocab' => $obo_name]), 'error');
  371. }
  372. }
  373. }
  374. /**
  375. * @see TripalImporter::formValidate()
  376. */
  377. public function formValidate($form, &$form_state) {
  378. $obo_id = $form_state['values']['obo_id'];
  379. $obo_name = trim($form_state['values']['obo_name']);
  380. $obo_url = trim($form_state['values']['obo_url']);
  381. $obo_file = trim($form_state['values']['obo_file']);
  382. $uobo_name = array_key_exists('uobo_name', $form_state['values']) ? trim($form_state['values']['uobo_name']) : '';
  383. $uobo_url = array_key_exists('uobo_url', $form_state['values']) ? trim($form_state['values']['uobo_url']) : '';
  384. $uobo_file = array_key_exists('uobo_file', $form_state['values']) ? trim($form_state['values']['uobo_file']) : '';
  385. // Make sure if the name is changed it doesn't conflict with another OBO.
  386. if ($form_state['clicked_button']['#name'] == 'update_obo_details' or
  387. $form_state['clicked_button']['#name'] == 'update_load_obo') {
  388. // Get the current record
  389. $vocab = db_select('tripal_cv_obo', 't')
  390. ->fields('t', ['obo_id', 'name', 'path'])
  391. ->condition('name', $uobo_name)
  392. ->execute()
  393. ->fetchObject();
  394. if ($vocab and $vocab->obo_id != $obo_id) {
  395. form_set_error('uobo_name', 'The vocabulary name must be different from existing vocabularies');
  396. }
  397. // Make sure the file exists. First check if it is a relative path
  398. $dfile = $_SERVER['DOCUMENT_ROOT'] . base_path() . $uobo_file;
  399. if (!file_exists($dfile)) {
  400. if (!file_exists($uobo_file)) {
  401. form_set_error('uobo_file', t('The specified path, !path, does not exist or cannot be read.'), ['!path' => $dfile]);
  402. }
  403. }
  404. if (!$uobo_url and !$uobo_file) {
  405. form_set_error('uobo_url', 'Please provide a URL or a path for the vocabulary.');
  406. }
  407. if ($uobo_url and $uobo_file) {
  408. form_set_error('uobo_url', 'Please provide only a URL or a path for the vocabulary, but not both.');
  409. }
  410. }
  411. if ($form_state['clicked_button']['#name'] == 'add_new_obo') {
  412. // Get the current record
  413. $vocab = db_select('tripal_cv_obo', 't')
  414. ->fields('t', ['obo_id', 'name', 'path'])
  415. ->condition('name', $obo_name)
  416. ->execute()
  417. ->fetchObject();
  418. if ($vocab) {
  419. form_set_error('obo_name', 'The vocabulary name must be different from existing vocabularies');
  420. }
  421. // Make sure the file exists. First check if it is a relative path
  422. $dfile = $_SERVER['DOCUMENT_ROOT'] . base_path() . $obo_file;
  423. if (!file_exists($dfile)) {
  424. if (!file_exists($obo_file)) {
  425. form_set_error('obo_file', t('The specified path, !path, does not exist or cannot be read.'), ['!path' => $dfile]);
  426. }
  427. }
  428. if (!$obo_url and !$obo_file) {
  429. form_set_error('obo_url', 'Please provide a URL or a path for the vocabulary.');
  430. }
  431. if ($obo_url and $obo_file) {
  432. form_set_error('obo_url', 'Please provide only a URL or a path for the vocabulary, but not both.');
  433. }
  434. }
  435. }
  436. /**
  437. * @see TripalImporter::run()
  438. *
  439. * @param $details
  440. * The following arguments are supported:
  441. * - obo_id: (required) The ID of the ontology to be imported.
  442. */
  443. public function run() {
  444. $arguments = $this->arguments['run_args'];
  445. $obo_id = $arguments['obo_id'];
  446. // Make sure the $obo_id is valid
  447. $obo = db_select('tripal_cv_obo', 'tco')
  448. ->fields('tco')
  449. ->condition('obo_id', $obo_id)
  450. ->execute()
  451. ->fetchObject();
  452. if (!$obo) {
  453. throw new Exception("Invalid OBO ID provided: '$obo_id'.");
  454. }
  455. // Get the list of all CVs so we can save on lookups later
  456. $sql = "SELECT * FROM {cv} CV";
  457. $cvs = chado_query($sql);
  458. while ($cv = $cvs->fetchObject()) {
  459. $this->all_cvs[$cv->name] = $cv;
  460. }
  461. // Get the list of all DBs so we can save on lookups later
  462. $sql = "SELECT * FROM {db} DB";
  463. $dbs = chado_query($sql);
  464. while ($db = $dbs->fetchObject()) {
  465. $this->all_dbs[$db->name] = $db;
  466. }
  467. // Get the 'Subgroup' term that we will use for adding subsets.
  468. $term = chado_get_cvterm(['id' => 'NCIT:C25693']);
  469. $this->used_terms['NCIT:C25693'] = $term->cvterm_id;
  470. // Get the 'Comment' term that we will use for adding comments.
  471. $term = chado_get_cvterm(['id' => 'rdfs:comment']);
  472. $this->used_terms['rdfs:comment'] = $term->cvterm_id;
  473. // Make sure we have a 'synonym_type' vocabulary.
  474. $syn_cv = new ChadoRecord('cv');
  475. $syn_cv->setValues(['name' => 'synonym_type']);
  476. $syn_cv->save();
  477. $this->all_cvs['synonym_type'] = (object) $syn_cv->getValues();
  478. // Make sure we have a 'synonym_type' database.
  479. $syn_db = new ChadoRecord('db');
  480. $syn_db->setValues(['name' => 'synonym_type']);
  481. $syn_db->save();
  482. $this->all_dbs['synonym_type'] = (object) $syn_db->getValues();
  483. // Make sure the synonym types exists in the 'synonym_type' vocabulary.
  484. foreach (array_keys($this->syn_types) as $syn_type) {
  485. $syn_dbxref = new ChadoRecord('dbxref');
  486. $syn_dbxref->setValues([
  487. 'accession' => $syn_type,
  488. 'db_id' => $syn_db->getID(),
  489. ]);
  490. $syn_dbxref->save();
  491. $syn_term = new ChadoRecord('cvterm');
  492. $syn_term->setValues([
  493. 'name' => $syn_type,
  494. 'cv_id' => $syn_cv->getID(),
  495. ]);
  496. if (!$syn_term->find()) {
  497. $syn_term->setValues([
  498. 'name' => $syn_type,
  499. 'definition' => '',
  500. 'is_obsolete' => 0,
  501. 'cv_id' => $syn_cv->getID(),
  502. 'is_relationshiptype' => 0,
  503. 'dbxref_id' => $syn_dbxref->getID(),
  504. ]);
  505. $syn_term->insert();
  506. }
  507. $this->syn_types[$syn_type] = (object) $syn_term->getValues();
  508. }
  509. // Run the importer!
  510. $this->loadOBO_v1_2_id($obo);
  511. }
  512. /**
  513. * @see TripalImporter::postRun()
  514. *
  515. */
  516. public function postRun() {
  517. // Clear the cached terms
  518. cache_clear_all('tripal_chado:term:*', 'cache', TRUE);
  519. // Update the cv_root_mview materialized view.
  520. $this->logMessage("Updating the cv_root_mview materialized view...");
  521. $mview_id = tripal_get_mview_id('cv_root_mview');
  522. tripal_populate_mview($mview_id);
  523. $this->logMessage("Updating the db2cv_mview materialized view...");
  524. $mview_id = tripal_get_mview_id('db2cv_mview');
  525. tripal_populate_mview($mview_id);
  526. // Update the cvtermpath table for each newly added CV.
  527. $this->logMessage("Updating cvtermpath table. This may take a while...");
  528. foreach ($this->obo_namespaces as $namespace => $cv_id) {
  529. $this->logMessage("- Loading paths for vocabulary: @vocab", ['@vocab' => $namespace]);
  530. chado_update_cvtermpath($cv_id, $this->job);
  531. }
  532. }
  533. /**
  534. * A wrapper function for importing the user specified OBO file into Chado by
  535. * specifying the obo_id of the OBO. It requires that the file be in OBO v1.2
  536. * compatible format. This function is typically executed via the Tripal
  537. * jobs
  538. * management after a user submits a job via the Load Ontologies form.
  539. *
  540. * @param $obo_id
  541. * An obo_id from the tripal_cv_obo file that specifies which OBO file to
  542. * import
  543. *
  544. * @ingroup tripal_obo_loader
  545. */
  546. private function loadOBO_v1_2_id($obo) {
  547. // Convert the module name to the real path if present
  548. if (preg_match("/\{(.*?)\}/", $obo->path, $matches)) {
  549. $module = $matches[1];
  550. $path = drupal_realpath(drupal_get_path('module', $module));
  551. $obo->path = preg_replace("/\{.*?\}/", $path, $obo->path);
  552. }
  553. // if the reference is for a remote URL then run the URL processing function
  554. if (preg_match("/^https:\/\//", $obo->path) or
  555. preg_match("/^http:\/\//", $obo->path) or
  556. preg_match("/^ftp:\/\//", $obo->path)) {
  557. $this->loadOBO_v1_2_url($obo->name, $obo->path, 0);
  558. }
  559. // if the reference is for a local file then run the file processing function
  560. else {
  561. // check to see if the file is located local to Drupal
  562. $dfile = $_SERVER['DOCUMENT_ROOT'] . base_path() . $obo->path;
  563. if (file_exists($dfile)) {
  564. $this->loadOBO_v1_2_file($obo->name, $dfile, 0);
  565. }
  566. // if not local to Drupal, the file must be someplace else, just use
  567. // the full path provided
  568. else {
  569. if (file_exists($obo->path)) {
  570. $this->loadOBO_v1_2_file($obo->name, $obo->path, 0);
  571. }
  572. else {
  573. print "ERROR: could not find OBO file: '$obo->path'\n";
  574. }
  575. }
  576. }
  577. }
  578. /**
  579. * A wrapper function for importing the user specified OBO file into Chado by
  580. * specifying the filename and path of the OBO. It requires that the file be
  581. * in OBO v1.2 compatible format. This function is typically executed via
  582. * the Tripal jobs management after a user submits a job via the Load
  583. * Ontologies form.
  584. *
  585. * @param $obo_name
  586. * The name of the OBO (typically the ontology or controlled vocabulary
  587. * name)
  588. * @param $file
  589. * The path on the file system where the ontology can be found
  590. * @param $is_new
  591. * Set to TRUE if this is a new ontology that does not yet exist in the
  592. * tripal_cv_obo table. If TRUE the OBO will be added to the table.
  593. *
  594. * @ingroup tripal_obo_loader
  595. */
  596. private function loadOBO_v1_2_file($obo_name, $file, $is_new = TRUE) {
  597. if ($is_new) {
  598. tripal_insert_obo($obo_name, $file);
  599. }
  600. $success = $this->loadOBO_v1_2($file, $obo_name);
  601. }
  602. /**
  603. * A wrapper function for importing the user specified OBO file into Chado by
  604. * specifying the remote URL of the OBO. It requires that the file be in OBO
  605. * v1.2 compatible format. This function is typically executed via the
  606. * Tripal jobs management after a user submits a job via the Load Ontologies
  607. * form.
  608. *
  609. * @param $obo_name
  610. * The name of the OBO (typically the ontology or controlled vocabulary
  611. * name)
  612. * @param $url
  613. * The remote URL of the OBO file.
  614. * @param $is_new
  615. * Set to TRUE if this is a new ontology that does not yet exist in the
  616. * tripal_cv_obo table. If TRUE the OBO will be added to the table.
  617. *
  618. * @ingroup tripal_obo_loader
  619. */
  620. private function loadOBO_v1_2_url($obo_name, $url, $is_new = TRUE) {
  621. // first download the OBO
  622. $temp = tempnam(sys_get_temp_dir(), 'obo_');
  623. print "Downloading URL $url, saving to $temp\n";
  624. $url_fh = fopen($url, "r");
  625. $obo_fh = fopen($temp, "w");
  626. if (!$url_fh) {
  627. throw new Exception("Unable to download the remote OBO file at $url. Could a firewall be blocking outgoing connections? " .
  628. " if you are unable to download the file you may manually download the OBO file and use the web interface to " .
  629. " specify the location of the file on your server.");
  630. }
  631. while (!feof($url_fh)) {
  632. fwrite($obo_fh, fread($url_fh, 255), 255);
  633. }
  634. fclose($url_fh);
  635. fclose($obo_fh);
  636. if ($is_new) {
  637. tripal_insert_obo($obo_name, $url);
  638. }
  639. // second, parse the OBO
  640. $this->loadOBO_v1_2($temp, $obo_name);
  641. // now remove the temp file
  642. unlink($temp);
  643. }
  644. /**
  645. * Imports a given OBO file into Chado. This function is usually called by
  646. * one of three wrapper functions: loadOBO_v1_2_id,
  647. * loadOBO_v1_2_file or tripal_cv_load_obo_v1_2_url. But, it can
  648. * be called directly if the full path to an OBO file is available on the
  649. * file system.
  650. *
  651. * @param $file
  652. * The full path to the OBO file on the file system
  653. *
  654. * @ingroup tripal_obo_loader
  655. */
  656. private function loadOBO_v1_2($file, $obo_name) {
  657. $header = [];
  658. $ret = [];
  659. // Empty the temp table.
  660. $this->clearTermStanzaCache();
  661. // Parse the obo file.
  662. $this->logMessage("Step 1: Preloading File $file...");
  663. $this->parse($file, $header);
  664. // Cache the relationships of terms.
  665. $this->logMessage("Step 2: Examining relationships...");
  666. $this->cacheRelationships();
  667. // Add any typedefs to the vocabulary first.
  668. $this->logMessage("Step 3: Loading type defs...");
  669. $this->processTypeDefs();
  670. // Next add terms to the vocabulary.
  671. $this->logMessage("Step 4: Loading terms...");
  672. $this->processTerms();
  673. // Empty the term cache.
  674. $this->logMessage("Step 5: Cleanup...");
  675. $this->clearTermStanzaCache();
  676. }
  677. /**
  678. * OBO files are divided into a typedefs terms section and vocabulary terms
  679. * section. This function loads the typedef terms from the OBO.
  680. *
  681. * @ingroup tripal_obo_loader
  682. */
  683. private function processTypeDefs() {
  684. $typedefs = $this->getCachedTermStanzas('Typedef');
  685. $count = $this->getCacheSize('Typedef');
  686. $this->setTotalItems($count);
  687. $this->setItemsHandled(0);
  688. $this->setInterval(5);
  689. $i = 1;
  690. foreach ($typedefs as $t) {
  691. // TODO: it would be better if we had a term iterator so that we
  692. // don't have to distinguish here between the table vs memory cache type.
  693. if ($this->cache_type == 'table') {
  694. $stanza = unserialize(base64_decode($t->stanza));
  695. }
  696. else {
  697. $stanza = $this->termStanzaCache['ids'][$t];
  698. }
  699. $this->setItemsHandled($i++);
  700. $this->processTerm($stanza, TRUE);
  701. }
  702. $this->setItemsHandled($i);
  703. return 1;
  704. }
  705. /**
  706. * This function loads all of the [Term] terms from the OBO.
  707. */
  708. private function processTerms() {
  709. $i = 0;
  710. $external = FALSE;
  711. $terms = $this->getCachedTermStanzas('Term');
  712. $count = $this->getCacheSize('Term');
  713. $this->setTotalItems($count);
  714. $this->setItemsHandled(0);
  715. $this->setInterval(1);
  716. // Iterate through the terms.
  717. foreach ($terms as $t) {
  718. // TODO: it would be better if we had a term iterator so that we
  719. // don't have to distinguish here between the table vs memory cache type.
  720. if ($this->cache_type == 'table') {
  721. $term = unserialize(base64_decode($t->stanza));
  722. }
  723. else {
  724. $term = $this->termStanzaCache['ids'][$t];
  725. }
  726. $this->setItemsHandled($i);
  727. // Add/update this term.
  728. $this->processTerm($term, FALSE);
  729. $i++;
  730. }
  731. $this->setItemsHandled($i);
  732. return 1;
  733. }
  734. /**
  735. * Sets the default CV and DB for this loader.
  736. *
  737. * Unfortunately, not all OBOs include both the 'ontology' and the
  738. * 'default-namespace' in their headers, so we have to do our best to
  739. * work out what these two should be.
  740. *
  741. */
  742. private function setDefaults($header) {
  743. $short_name = '';
  744. $namespace = '';
  745. $idspaces = [];
  746. // Get the 'ontology' and 'default-namespace' headers. Unfortunately,
  747. // not all OBO files contain these.
  748. if (array_key_exists('ontology', $header)) {
  749. $short_name = strtoupper($header['ontology'][0]);
  750. }
  751. if (array_key_exists('default-namespace', $header)) {
  752. $namespace = $header['default-namespace'][0];
  753. }
  754. if (array_key_exists('idspace', $header)) {
  755. $matches = [];
  756. foreach ($header['idspace'] as $idspace) {
  757. if (preg_match('/^(.+?)\s+(.+?)\s+"(.+)"$/', $idspace, $matches)) {
  758. $idspaces[$matches[1]]['url'] = $matches[2];
  759. $idspaces[$matches[1]]['description'] = $matches[3];
  760. }
  761. elseif (preg_match('/^(.+?)\s+(.+?)$/', $idspace, $matches)) {
  762. $idspaces[$matches[1]]['url'] = $matches[2];
  763. $idspaces[$matches[1]]['description'] = '';
  764. }
  765. }
  766. }
  767. // The OBO specification allows the 'ontology' header tag to be nested for
  768. // subsets (e.g. go/subsets/goslim_plant). We need to simplify that down
  769. // to the top-level item.
  770. $matches = [];
  771. if (preg_match('/^(.+?)\/.*/', $short_name, $matches)) {
  772. $short_name = $matches[1];
  773. $this->is_subset = TRUE;
  774. }
  775. // If we have the DB short name (or ontology header) but not the default
  776. // namespace then we may be able to find it via an EBI lookup.
  777. if (!$namespace and $short_name) {
  778. $namespace = $this->findEBIOntologyNamespace($short_name);
  779. }
  780. // If we have the namespace but not the short name then we have to
  781. // do a few tricks to try and find it.
  782. if ($namespace and !$short_name) {
  783. // First see if we've seen this ontology before and get it's currently
  784. // loaded database.
  785. $sql = "SELECT dbname FROM {db2cv_mview} WHERE cvname = :cvname";
  786. $short_name = chado_query($sql, [':cvname' => $namespace])->fetchField();
  787. if (!$short_name and array_key_exists('namespace-id-rule', $header)) {
  788. $matches = [];
  789. if (preg_match('/^.*\s(.+?):.+$/', $header['namespace-id-rule'][0], $matches)) {
  790. $short_name = $matches[1];
  791. }
  792. }
  793. // Try the EBI Lookup: still experimental.
  794. if (!$short_name) {
  795. //$short_name = $this->findEBIOntologyPrefix($namespace);
  796. }
  797. }
  798. // If we still don't have a namespace defined, use the one from the form
  799. // in the "New Vocabulary Name" field
  800. if (!$namespace and array_key_exists('run_args', $this->arguments)
  801. and array_key_exists('obo_name', $this->arguments['run_args'])) {
  802. $namespace = $this->arguments['run_args']['obo_name'];
  803. }
  804. if (!$namespace and array_key_exists('run_args', $this->arguments)
  805. and array_key_exists('uobo_name', $this->arguments['run_args'])) {
  806. $namespace = $this->arguments['run_args']['uobo_name'];
  807. }
  808. // If we can't find the namespace or the short_name then bust.
  809. if (!$namespace and !$short_name) {
  810. throw new ErrorException('Cannot determine the namespace or ontology prefix from this OBO file. It is missing both the "default-namespace" or a compatible "ontology" header.');
  811. }
  812. // Set the defaults.
  813. $this->default_namespace = $namespace;
  814. $this->default_db = $short_name;
  815. $this->addDB($this->default_db);
  816. $cv = $this->addCV($this->default_namespace);
  817. $this->obo_namespaces[$namespace] = $cv->cv_id;
  818. $this->idspaces = $idspaces;
  819. // Add a new database for each idspace.
  820. foreach ($idspaces as $shortname => $idspace) {
  821. $this->addDB($shortname, $idspace['url'], $idspace['description']);
  822. }
  823. }
  824. /**
  825. * This function searches EBI to find the ontology details for this OBO.
  826. *
  827. * @param $ontology
  828. * The ontology name from the OBO headers.
  829. *
  830. * @throws Exception
  831. */
  832. private function findEBIOntologyNamespace($ontology) {
  833. // Check if the EBI ontology search has this ontology:
  834. try {
  835. $results = $this->oboEbiLookup($ontology, 'ontology');
  836. if ($results and array_key_exists('config', $results) and array_key_exists('default-namespace', $results['config']['annotations'])) {
  837. $namespace = $results['config']['annotations']['default-namespace'];
  838. if (is_array($namespace)) {
  839. $namespace = $namespace[0];
  840. }
  841. }
  842. elseif ($results and array_key_exists('config', $results) and array_key_exists('namespace', $results['config'])) {
  843. $namespace = $results['config']['namespace'];
  844. }
  845. // If we can't find the namespace at EBI, then just default to using the
  846. // same namespace as the DB short name.
  847. else {
  848. $namespace = $this->default_db;
  849. }
  850. return $namespace;
  851. } catch (Exception $e) {
  852. watchdog_exception('Cannot find the namespace for this ontology.', $e);
  853. throw $e;
  854. }
  855. }
  856. /**
  857. * Finds the ontology prefix (DB short name) using EBI.
  858. *
  859. * @param $namespace
  860. * The namespace for ontology.
  861. */
  862. private function findEBIOntologyPrefix($namespace) {
  863. // NOTE: this code is not yet completed.. It's not clear it will
  864. // actually work.
  865. $options = [];
  866. $page = 1;
  867. $size = 25;
  868. $full_url = 'https://www.ebi.ac.uk/ols/api/ontologies?page=' . $page . '&size=' . $size;
  869. while ($response = drupal_http_request($full_url, $options)) {
  870. $response = drupal_json_decode($response->data);
  871. foreach ($response['_embedded']['ontologies'] as $ontology) {
  872. $namespace = $ontology['config']['namespace'];
  873. }
  874. $page++;
  875. $full_url = 'https://www.ebi.ac.uk/ols/api/ontologies?page=' . $page . '&size=' . $size;
  876. }
  877. }
  878. /**
  879. * A helper function to get details about a foreign term.
  880. *
  881. * A foreign term is one that does not belong to the ontology.
  882. *
  883. * @param $t
  884. * A term array that contains these keys at a minimum: id, name,
  885. * definition, subset, namespace, is_obsolete.
  886. */
  887. private function findEBITerm($id) {
  888. // Warn the user if we're looking up terms in EBI as this will slow the
  889. // loader if there are many lookups.
  890. if ($this->ebi_warned == FALSE) {
  891. $this->logMessage(
  892. "A term that belongs to another ontology is used within this " .
  893. "vocabulary. Therefore a lookup will be performed with the EBI Ontology " .
  894. "Lookup Service to retrieve the information for this term. " .
  895. "Please note, that vocabularies with many non-local terms " .
  896. "require remote lookups and these lookups can dramatically " .
  897. "increase loading time. ",
  898. ['!vocab' => $this->default_namespace], TRIPAL_WARNING);
  899. $this->ebi_warned = TRUE;
  900. // This ontology may have multiple remote terms and that takes a while
  901. // to load so lets change the progress interval down to give
  902. // updates more often.
  903. $this->setInterval(1);
  904. }
  905. $this->logMessage("Performing EBI OLS Lookup for: !id", ['!id' => $id]);
  906. // Get the short name and accession for the term.
  907. $pair = explode(":", $id, 2);
  908. $short_name = $pair[0];
  909. $accession = $pair[1];
  910. // First get the ontology so we can build an IRI for the term
  911. $base_iri = '';
  912. $ontologyID = '';
  913. if (array_key_exists($short_name, $this->baseIRIs)) {
  914. list($ontologyID, $base_iri) = $this->baseIRIs[$short_name];
  915. }
  916. else {
  917. $full_url = 'http://www.ebi.ac.uk/ols/api/ontologies/' . $short_name;
  918. $response = drupal_http_request($full_url, []);
  919. if (!$response) {
  920. throw new Exception(t('Did not get a response from EBI OLS trying to lookup ontology: !ontology',
  921. ['!ontology' => $short_name]));
  922. }
  923. $ontology_results = drupal_json_decode($response->data);
  924. if ($ontology_results['error']) {
  925. $this->logMessage(t('Cannot find the ontology via an EBI OLS lookup: !short_name. \n' .
  926. 'We tried to access: !url' .
  927. 'EBI Reported: !message. ' .
  928. 'Consider finding the OBO file for this ontology and manually loading it first.',
  929. [
  930. '!message' => $ontology_results['message'],
  931. '!short_name' => $short_name,
  932. '!url' => $full_url,
  933. ]), TRIPAL_WARNING);
  934. }
  935. //What should happen with this stuff?
  936. $base_iri = $ontology_results['config']['baseUris'][0];
  937. $ontologyID = $ontology_results['ontologyId'];
  938. $this->baseIRIs[$short_name] = [$ontologyID, $base_iri];
  939. }
  940. // Next get the term.
  941. $iri = urlencode(urlencode($base_iri . $accession));
  942. $full_url = 'http://www.ebi.ac.uk/ols/api/ontologies/' . $ontologyID . '/terms/' . $iri;
  943. $response = drupal_http_request($full_url, []);
  944. if (!$response) {
  945. throw new Exception(t('Did not get a response from EBI OLS trying to lookup term: !id',
  946. ['!id' => $id]));
  947. }
  948. $results = drupal_json_decode($response->data);
  949. if (!$results) {
  950. $this->logMessage('Error: no data with !url. The response was: !response', [
  951. '!url' => $full_url,
  952. '!response' => $response,
  953. ]);
  954. return FALSE;
  955. }
  956. // If EBI sent an error message then throw an error.
  957. if ($results['error']) {
  958. $this->logMessage('Cannot find the term via an EBI OLS lookup: !term. ' .
  959. 'We tried to access: "!url" ' .
  960. 'EBI Reported: !message. Consider finding the OBO file for this ontology and manually loading it first.',
  961. [
  962. '!message' => $results['message'],
  963. '!term' => $id,
  964. '!url' => $full_url,
  965. ], TRIPAL_WARNING);
  966. return FALSE;
  967. }
  968. // TODO: what do we do if the term is not defined by this ontology?
  969. if ($results['is_defining_ontology'] != 1) {
  970. }
  971. // Make an OBO stanza array as if this term were in the OBO file and
  972. // return it.
  973. $this->logMessage("Found !term in EBI OLS.", ['!term' => $id]);
  974. $stanza = [];
  975. $stanza['id'][0] = $id;
  976. $stanza['name'][0] = $results['label'];
  977. $stanza['def'][0] = $results['def'];
  978. $stanza['namespace'][0] = $results['ontology_name'];
  979. $stanza['is_obsolete'][0] = $results['is_obsolete'] ? 'true' : '';
  980. $stanza['is_relationshiptype'][0] = '';
  981. $stanza['db_name'][0] = $short_name;
  982. $stanza['comment'][0] = 'Term obtained using the EBI Ontology Lookup Service.';
  983. if (array_key_exists('in_subset', $results)) {
  984. if (is_array($results['in_subset'])) {
  985. $stanza['subset'] = $results['in_subset'];
  986. }
  987. elseif ($results['in_subset']) {
  988. $stanza['subset'][0] = $results['in_subset'];
  989. }
  990. }
  991. // If this term has been replaced then get the new term.
  992. if (array_key_exists('term_replaced_by', $results) and isset($results['term_replaced_by'])) {
  993. $replaced_by = $results['term_replaced_by'];
  994. $replaced_by = preg_replace('/_/', ':', $replaced_by);
  995. $this->logMessage("The term, !term, is replaced by, !replaced",
  996. ['!term' => $id, '!replaced' => $replaced_by]);
  997. // Before we try to look for the replacement term, let's try to find it.
  998. // in our list of cached terms.
  999. if (array_key_exists($replaced_by, $this->termStanzaCache['ids'])) {
  1000. $this->logMessage("Found term, !replaced in the term cache.",
  1001. ['!term' => $id, '!replaced' => $replaced_by]);
  1002. return $this->termStanzaCache['ids'][$id];
  1003. }
  1004. // Next look in the database.
  1005. $rpair = explode(":", $replaced_by, 2);
  1006. $found = $this->lookupTerm($rpair[0], $rpair[1]);
  1007. if ($found) {
  1008. $this->logMessage("Found term, !replaced in the local data store.",
  1009. ['!term' => $id, '!replaced' => $replaced_by]);
  1010. return $found;
  1011. }
  1012. // Look for this new term.
  1013. $stanza = $this->findEBITerm($replaced_by);
  1014. }
  1015. return $stanza;
  1016. }
  1017. /**
  1018. * Inserts a new cvterm using the OBO stanza array provided.
  1019. *
  1020. * The stanza passed to this function should always come from the term cache,
  1021. * not directly from the OBO file because the cached terms have been
  1022. * updated to include all necessary values. This function also removes
  1023. * all properties associated with the term so that those can be added
  1024. * fresh.
  1025. *
  1026. * @param $stanza
  1027. * An OBO stanza array as returned by getCachedTermStanza().
  1028. * @param $is_relationship
  1029. * Set to TRUE if this term is a relationship term.
  1030. * @param $update_if_exists
  1031. * Set to TRUE to update the term if it exists.
  1032. *
  1033. * @return
  1034. * The cvterm ID.
  1035. */
  1036. private function saveTerm($stanza, $is_relationship = FALSE) {
  1037. // Get the term ID.
  1038. $id = $stanza['id'][0];
  1039. // First check if we've already used this term.
  1040. if (array_key_exists($id, $this->used_terms)) {
  1041. return $this->used_terms[$id];
  1042. }
  1043. // Get the term properties.
  1044. $id = $stanza['id'][0];
  1045. $name = $stanza['name'][0];
  1046. $cvname = $stanza['namespace'][0];
  1047. $dbname = $stanza['db_name'][0];
  1048. $namespace = $stanza['namespace'][0];
  1049. // Does this term ID have both a short name and accession? If so, then
  1050. // separate out these components, otherwise we will use the id as both
  1051. // the id and accession.
  1052. $accession = '';
  1053. $matches = [];
  1054. if (preg_match('/^(.+?):(.*)$/', $id, $matches)) {
  1055. $accession = $matches[2];
  1056. }
  1057. else {
  1058. $accession = $id;
  1059. }
  1060. // Get the definition if there is one.
  1061. $definition = '';
  1062. if (array_key_exists('def', $stanza)) {
  1063. $definition = preg_replace('/^\"(.*)\"/', '\1', $stanza['def'][0]);
  1064. }
  1065. // Set the flag if this term is obsolete.
  1066. $is_obsolete = 0;
  1067. if (array_key_exists('is_obsolete', $stanza)) {
  1068. $is_obsolete = $stanza['is_obsolete'][0] == 'true' ? 1 : 0;
  1069. }
  1070. // Set the flag if this is a relationship type.
  1071. $is_relationshiptype = 0;
  1072. if (array_key_exists('is_relationshiptype', $stanza)) {
  1073. $is_relationshiptype = $stanza['is_relationshiptype'][0] == 'true' ? 1 : 0;
  1074. }
  1075. // Is this term borrowed from another ontology?
  1076. $is_borrowed = $this->isTermBorrowed($stanza);
  1077. // Will hold the cvterm ChadoRecord object.
  1078. $cvterm = NULL;
  1079. // Get the CV and DB objects.
  1080. $cv = $this->all_cvs[$cvname];
  1081. $db = $this->all_dbs[$dbname];
  1082. // If this is set to TRUE then we should insert the term.
  1083. $do_cvterm_insert = TRUE;
  1084. // We need to locate terms using their dbxref. This is because term names
  1085. // can sometimes change, so we don't want to look up the term by it's name.
  1086. // the unique ID which is in the accession will never change.
  1087. $dbxref = new ChadoRecord('dbxref');
  1088. $dbxref->setValues([
  1089. 'db_id' => $db->db_id,
  1090. 'accession' => $accession,
  1091. ]);
  1092. if ($dbxref->find()) {
  1093. // Does this accession already have a cvterm it's associated with? Then
  1094. // we need to make we will update the name. Names change but accessions
  1095. // always refer to the same term.
  1096. $dbx_cvterm = new ChadoRecord('cvterm');
  1097. $dbx_cvterm->setValues(['dbxref_id' => $dbxref->getID()]);
  1098. if ($dbx_cvterm->find()) {
  1099. $do_cvterm_insert = FALSE;
  1100. $cvterm = $dbx_cvterm;
  1101. // We don't want to do any updates for borrowed terms. Just leave them
  1102. // as they are.
  1103. if (!$is_borrowed) {
  1104. // Let's make sure we don't have a conflict in term naming
  1105. // if we change the name of this term.
  1106. $this->fixTermMismatch($stanza, $dbxref, $cv, $name);
  1107. // Now update this cvterm record.
  1108. $cvterm->setValue('name', $name);
  1109. $cvterm->setValue('definition', $definition);
  1110. $cvterm->setValue('is_obsolete', $is_obsolete);
  1111. $cvterm->setValue('is_relationshiptype', $is_relationshiptype);
  1112. try {
  1113. $cvterm->update();
  1114. } catch (Exception $e) {
  1115. $this->logMessage('Could not update the term, "!term", with name, "!name" for vocabulary, "!vocab". ERROR: !error.',
  1116. [
  1117. '!term' => $id,
  1118. '!name' => $name,
  1119. '!vocab' => $cv->name,
  1120. '!error' => $e->getMessage(),
  1121. ],
  1122. TRIPAL_ERROR);
  1123. throw $e;
  1124. }
  1125. }
  1126. }
  1127. }
  1128. // The dbxref doesn't exist, so let's add it.
  1129. else {
  1130. $dbxref->insert();
  1131. }
  1132. // Add the cvterm if we didn't do an update.
  1133. if ($do_cvterm_insert) {
  1134. // Before updating the term let's check to see if it already exists
  1135. // and make corrections.
  1136. $cvterm = new ChadoRecord('cvterm');
  1137. $cvterm->setValue('cv_id', $cv->cv_id);
  1138. $cvterm->setValue('name', $name);
  1139. if ($cvterm->find()) {
  1140. $fixed = $this->fixTermMismatch($stanza, $dbxref, $cv, $name);
  1141. }
  1142. // The term doesn't exist, so let's just do our insert.
  1143. $cvterm->setValues([
  1144. 'cv_id' => $cv->cv_id,
  1145. 'name' => $name,
  1146. 'definition' => $definition,
  1147. 'dbxref_id' => $dbxref->getID(),
  1148. 'is_relationshiptype' => $is_relationshiptype,
  1149. 'is_obsolete' => $is_obsolete,
  1150. 'dbxref_id' => $dbxref->getValue('dbxref_id'),
  1151. ]);
  1152. // If the insert fails lets catch the error so we can
  1153. // give a more informative message.
  1154. try {
  1155. $cvterm->insert();
  1156. } catch (Exception $e) {
  1157. $this->logMessage('Could not insert the term, "!term", with name, "!name" for vocabulary, "!vocab". ERROR: !error.',
  1158. [
  1159. '!term' => $id,
  1160. '!name' => $name,
  1161. '!vocab' => $cv->name,
  1162. '!error' => $e->getMessage(),
  1163. ],
  1164. TRIPAL_ERROR);
  1165. throw $e;
  1166. }
  1167. }
  1168. // Save the cvterm_id for this term so we don't look it up again.
  1169. $cvterm_id = $cvterm->getID();
  1170. $this->used_terms[$id] = $cvterm_id;
  1171. // Return the cvterm_id.
  1172. return $cvterm_id;
  1173. }
  1174. /**
  1175. * Fixes mismatches between two terms with the same name.
  1176. *
  1177. * If it has been determined that a term's name has changed. Before we update
  1178. * or insert it we must check to make sure no other terms have that name. If
  1179. * they do we must make a correction.
  1180. *
  1181. * @param $dbxref
  1182. * The ChadoRecord object containing the dbxref record for the term
  1183. * to be inserted/updated.
  1184. * @param $cv
  1185. * The cvterm object.
  1186. * @param $name
  1187. * The name of the term that is a potential conflict.
  1188. *
  1189. * @return
  1190. * Returns TRUE if a conflict was found and corrected.
  1191. */
  1192. public function fixTermMismatch($stanza, $dbxref, $cv, $name) {
  1193. $id = $stanza['id'][0];
  1194. $name = $stanza['name'][0];
  1195. // First get the record for any potential conflicting term.
  1196. $sql = "
  1197. SELECT cvterm_id
  1198. FROM {cvterm}
  1199. WHERE name = :name and cv_id = :cv_id and dbxref_id != :dbxref_id
  1200. ";
  1201. $args = [
  1202. ':name' => $name,
  1203. ':cv_id' => $cv->cv_id,
  1204. ':dbxref_id' => $dbxref->getID(),
  1205. ];
  1206. $results = chado_query($sql, $args);
  1207. while ($conflict_id = $results->fetchField()) {
  1208. $check_cvterm = new ChadoRecord('cvterm', $conflict_id);
  1209. // If the dbxref of this matched term is the same as the current term
  1210. // then it is the same term and there is no conflict.
  1211. if ($dbxref->getID() == $check_cvterm->getValue('dbxref_id')) {
  1212. return FALSE;
  1213. }
  1214. // At this point, we have a cvterm with the same name and vocabulary
  1215. // but with a different dbxref. First let's get that other accession.
  1216. $check_dbxref = new ChadoRecord('dbxref', $check_cvterm->getValue('dbxref_id'));
  1217. $check_db = new ChadoRecord('db', $check_dbxref->getValue('db_id'));
  1218. $check_accession = $check_db->getValue('name') . ':' . $check_dbxref->getValue('accession');
  1219. // Case 1: The other term that currently has the same name is
  1220. // missing in the OBO file (i.e. no stanza). So, that means that this
  1221. // term probably got relegated to an alt_id on another term. We do
  1222. // not want to delete a term because it may be linked to other
  1223. // records. Instead, let's update its name to let folks know
  1224. // what happened to it and so we can get around the unique
  1225. // constraint. An example of this is the GO:0015881 and
  1226. // GO:1902598 terms where the latter became an alt_id of the
  1227. // first and no longer has its own entry.
  1228. $check_stanza = $this->getCachedTermStanza($check_accession);
  1229. if (!$check_stanza) {
  1230. $new_name = $check_cvterm->getValue('name') . ' (' . $check_accession . ')';
  1231. $check_cvterm->setValue('name', $new_name);
  1232. $check_cvterm->setValue('is_obsolete', '1');
  1233. $check_cvterm->update();
  1234. return TRUE;
  1235. }
  1236. // Case 2: The conflicting term is in the OBO file (ie. has a stanza) and
  1237. // is obsolete and this one is not. Fix it by adding an (obsolete) suffix
  1238. // to the name to avoid the conflict.
  1239. else {
  1240. if (array_key_exists('is_obsolete', $check_stanza) and ($check_stanza['is_obsolete'][0] == 'true') and (!array_key_exists('is_obsolete', $stanza) or ($stanza['is_obsolete'][0] != 'true'))) {
  1241. $new_name = $check_cvterm->getValue('name') . ' (obsolete)';
  1242. $check_cvterm->setValue('name', $new_name);
  1243. $check_cvterm->update();
  1244. return TRUE;
  1245. }
  1246. // Case 3: The conflicting term is in the OBO file (ie. has a stanza).
  1247. // That means that there has been some name swapping between
  1248. // terms. We need to temporarily rename the term so that
  1249. // we don't have a unique constraint violation when we update
  1250. // the new one. An example of this is where GO:000425 and
  1251. // GO:0030242 changed names and one was renamed to the previous
  1252. // name of the other.
  1253. else {
  1254. $new_name = $check_cvterm->getValue('name') . ' (' . $check_accession . ')';
  1255. $check_cvterm->setValue('name', $new_name);
  1256. $check_cvterm->update();
  1257. return TRUE;
  1258. }
  1259. }
  1260. }
  1261. // We have no conflict so it's save to update or insert.
  1262. return FALSE;
  1263. }
  1264. /**
  1265. * Uses the provided term array to add/update information to Chado about the
  1266. * term including the term, dbxref, synonyms, properties, and relationships.
  1267. *
  1268. * @param $term
  1269. * An array representing the cvterm.
  1270. *
  1271. * @is_relationship
  1272. * Set to 1 if this term is a relationship term
  1273. *
  1274. * @ingroup tripal_obo_loader
  1275. */
  1276. private function processTerm($stanza, $is_relationship = 0) {
  1277. //
  1278. // First things first--save the term.
  1279. //
  1280. // If the term does not exist it is inserted, if it does exist it just
  1281. // retrieves the cvterm_id.
  1282. //
  1283. $cvterm_id = $this->saveTerm($stanza, FALSE);
  1284. $id = $stanza['id'][0];
  1285. // If this term is borrowed from another ontology? If so then we will
  1286. // not update it.
  1287. if ($this->isTermBorrowed($stanza)) {
  1288. return;
  1289. }
  1290. // If this term belongs to this OBO (not borrowed from another OBO) then
  1291. // remove any relationships, properties, xrefs, and synonyms that this
  1292. // term already has so that they can be re-added.
  1293. $sql = "
  1294. DELETE FROM {cvterm_relationship}
  1295. WHERE subject_id = :cvterm_id
  1296. ";
  1297. chado_query($sql, [':cvterm_id' => $cvterm_id]);
  1298. // If this is an obsolete term then clear out the relationships where
  1299. // this term is the object.
  1300. if (in_array('is_obsolete', $stanza) and $stanza['is_obsolete'] == 'true') {
  1301. $sql = "
  1302. DELETE FROM {cvterm_relationship}
  1303. WHERE object_id = :cvterm_id
  1304. ";
  1305. chado_query($sql, [':cvterm_id' => $cvterm_id]);
  1306. }
  1307. $sql = "
  1308. DELETE FROM {cvtermprop}
  1309. WHERE cvterm_id = :cvterm_id
  1310. ";
  1311. chado_query($sql, [':cvterm_id' => $cvterm_id]);
  1312. $sql = "
  1313. DELETE FROM {cvterm_dbxref}
  1314. WHERE cvterm_id = :cvterm_id
  1315. ";
  1316. chado_query($sql, [':cvterm_id' => $cvterm_id]);
  1317. $sql = "
  1318. DELETE FROM {cvtermsynonym} CVTSYN
  1319. WHERE cvterm_id = :cvterm_id
  1320. ";
  1321. chado_query($sql, [':cvterm_id' => $cvterm_id]);
  1322. // We should never have the problem where we don't have a cvterm_id. The
  1323. // saveTerm() function should always return on. But if for some unknown
  1324. // reason we don't have one then fail.
  1325. if (!$cvterm_id) {
  1326. throw new Exception(t('Missing cvterm after saving term: !term',
  1327. ['!term' => print_r($stanza, TRUE)]));
  1328. }
  1329. //
  1330. // Handle: alt_id
  1331. //
  1332. if (array_key_exists('alt_id', $stanza)) {
  1333. foreach ($stanza['alt_id'] as $alt_id) {
  1334. $this->addAltID($id, $cvterm_id, $alt_id);
  1335. }
  1336. }
  1337. //
  1338. // Handle: synonym
  1339. //
  1340. if (array_key_exists('synonym', $stanza)) {
  1341. foreach ($stanza['synonym'] as $synonym) {
  1342. $this->addSynonym($id, $cvterm_id, $synonym);
  1343. }
  1344. }
  1345. //
  1346. // Handle: exact_synonym
  1347. //
  1348. if (array_key_exists('exact_synonym', $stanza)) {
  1349. foreach ($stanza['exact_synonym'] as $synonym) {
  1350. $fixed = preg_replace('/^\s*(\".+?\")(.*?)$/', '$1 EXACT $2', $synonym);
  1351. $this->addSynonym($id, $cvterm_id, $fixed);
  1352. }
  1353. }
  1354. //
  1355. // Handle: narrow_synonym
  1356. //
  1357. if (array_key_exists('narrow_synonym', $stanza)) {
  1358. foreach ($stanza['narrow_synonym'] as $synonym) {
  1359. $fixed = preg_replace('/^\s*(\".+?\")(.*?)$/', '$1 NARROW $2', $synonym);
  1360. $this->addSynonym($id, $cvterm_id, $fixed);
  1361. }
  1362. }
  1363. //
  1364. // Handle: broad_synonym
  1365. //
  1366. if (array_key_exists('broad_synonym', $stanza)) {
  1367. foreach ($stanza['broad_synonym'] as $synonym) {
  1368. $fixed = preg_replace('/^\s*(\".+?\")(.*?)$/', '$1 BROAD $2', $synonym);
  1369. $this->addSynonym($id, $cvterm_id, $fixed);
  1370. }
  1371. }
  1372. //
  1373. // Handle: comment
  1374. //
  1375. if (array_key_exists('comment', $stanza)) {
  1376. $comments = $stanza['comment'];
  1377. foreach ($comments as $rank => $comment) {
  1378. $this->addComment($id, $cvterm_id, $comment, $rank);
  1379. }
  1380. }
  1381. //
  1382. // Handle: xref
  1383. //
  1384. if (array_key_exists('xref', $stanza)) {
  1385. foreach ($stanza['xref'] as $xref) {
  1386. $this->addXref($id, $cvterm_id, $xref);
  1387. }
  1388. }
  1389. //
  1390. // Handle: xref_analog
  1391. //
  1392. if (array_key_exists('xref_analog', $stanza)) {
  1393. foreach ($stanza['xref_analog'] as $xref) {
  1394. $this->addXref($id, $cvterm_id, $xref);
  1395. }
  1396. }
  1397. //
  1398. // Handle: xref_unk
  1399. //
  1400. if (array_key_exists('xref_unk', $stanza)) {
  1401. foreach ($stanza['xref_unk'] as $xref) {
  1402. $this->addXref($id, $cvterm_id, $xref);
  1403. }
  1404. }
  1405. //
  1406. // Handle: subset
  1407. //
  1408. if (array_key_exists('subset', $stanza)) {
  1409. foreach ($stanza['subset'] as $subset) {
  1410. $this->addSubset($id, $cvterm_id, $subset);
  1411. }
  1412. }
  1413. //
  1414. // Handle: is_a
  1415. //
  1416. if (array_key_exists('is_a', $stanza)) {
  1417. foreach ($stanza['is_a'] as $is_a) {
  1418. $this->addRelationship($id, $cvterm_id, 'is_a', $is_a);
  1419. }
  1420. }
  1421. //
  1422. // Handle: relationship
  1423. //
  1424. if (array_key_exists('relationship', $stanza)) {
  1425. foreach ($stanza['relationship'] as $value) {
  1426. $rel = preg_replace('/^(.+?)\s.+?$/', '\1', $value);
  1427. $object = preg_replace('/^.+?\s(.+?)$/', '\1', $value);
  1428. $this->addRelationship($id, $cvterm_id, $rel, $object);
  1429. }
  1430. }
  1431. /**
  1432. * The following properties are currently unsupported:
  1433. *
  1434. * - intersection_of
  1435. * - union_of
  1436. * - disjoint_from
  1437. * - replaced_by
  1438. * - consider
  1439. * - use_term
  1440. * - builtin
  1441. * - is_anonymous
  1442. *
  1443. */
  1444. }
  1445. /**
  1446. * Adds a cvterm relationship
  1447. *
  1448. * @param $cvterm_id
  1449. * A cvterm_id of the term to which the relationship will be added.
  1450. * @param $rel_id
  1451. * The relationship term ID
  1452. * @param $obj_id
  1453. * The relationship object term ID.
  1454. *
  1455. * @ingroup tripal_obo_loader
  1456. */
  1457. private function addRelationship($id, $cvterm_id, $rel_id, $obj_id) {
  1458. // Get the cached terms for both the relationship and the object. They
  1459. // should be there, but just in case something went wrong, we'll throw
  1460. // an exception if we can't find them.
  1461. $rel_stanza = $this->getCachedTermStanza($rel_id);
  1462. if (!$rel_stanza) {
  1463. throw new Exception(t('Cannot add relationship: "!source !rel !object". ' .
  1464. 'The term, !rel, is not in the term cache.',
  1465. ['!source' => $id, '!rel' => $rel_id, '!name' => $obj_id]));
  1466. }
  1467. $rel_cvterm_id = $this->saveTerm($rel_stanza, TRUE);
  1468. // Make sure the object term exists in the cache.
  1469. $obj_stanza = $this->getCachedTermStanza($obj_id);
  1470. if (!$obj_stanza) {
  1471. throw new Exception(t('Cannot add relationship: "!source !rel !object". ' .
  1472. 'The term, !object, is not in the term cache.',
  1473. ['!source' => $id, '!rel' => $rel_id, '!object' => $obj_id]));
  1474. }
  1475. $obj_cvterm_id = $this->saveTerm($obj_stanza);
  1476. // Add the cvterm_relationship.
  1477. $cvterm_relationship = new ChadoRecord('cvterm_relationship');
  1478. $cvterm_relationship->setValues([
  1479. 'type_id' => $rel_cvterm_id,
  1480. 'subject_id' => $cvterm_id,
  1481. 'object_id' => $obj_cvterm_id,
  1482. ]);
  1483. // If the insert fails then catch the error and generate a more meaningful
  1484. // message that helps with debugging.
  1485. try {
  1486. $cvterm_relationship->insert();
  1487. } catch (Exception $e) {
  1488. throw new Exception(t('Cannot add relationship: "!source !rel !object". ' .
  1489. 'ERROR: !error.',
  1490. [
  1491. '!source' => $id,
  1492. '!rel' => $rel_id,
  1493. '!object' => $obj_id,
  1494. '!error' => $e->getMessage(),
  1495. ]));
  1496. }
  1497. }
  1498. /**
  1499. * Retrieves the term array from the temp loading table for a given term id.
  1500. *
  1501. * @param id
  1502. * The id of the term to retrieve
  1503. *
  1504. * @ingroup tripal_obo_loader
  1505. */
  1506. private function getCachedTermStanza($id) {
  1507. if ($this->cache_type == 'table') {
  1508. $values = ['id' => $id];
  1509. $result = chado_select_record('tripal_obo_temp', ['stanza'], $values);
  1510. if (count($result) == 0) {
  1511. return FALSE;
  1512. }
  1513. return unserialize(base64_decode($result['stanza']));
  1514. }
  1515. if (array_key_exists($id, $this->termStanzaCache['ids'])) {
  1516. return $this->termStanzaCache['ids'][$id];
  1517. }
  1518. else {
  1519. return FALSE;
  1520. }
  1521. }
  1522. /**
  1523. * Using the term's short-name and accession try to find it in Chado.
  1524. *
  1525. * @param $short_name
  1526. * The term's ontology prefix (database short name)
  1527. * @param $accession
  1528. * The term's accession.
  1529. *
  1530. * @return array|NULL
  1531. */
  1532. private function lookupTerm($short_name, $accession) {
  1533. // Does the database already exist?
  1534. if (!array_key_exists($short_name, $this->all_dbs)) {
  1535. return NULL;
  1536. }
  1537. $db = $this->all_dbs[$short_name];
  1538. // Check if the dbxref exists.
  1539. $dbxref = new ChadoRecord('dbxref');
  1540. $dbxref->setValues([
  1541. 'db_id' => $db->db_id,
  1542. 'accession' => $accession,
  1543. ]);
  1544. if (!$dbxref->find()) {
  1545. return NULL;
  1546. }
  1547. // If the dbxref exists then see if it has a corresponding cvterm.
  1548. $cvterm = new ChadoRecord('cvterm');
  1549. $cvterm->setValues(['dbxref_id' => $dbxref->getID()]);
  1550. if (!$cvterm->find()) {
  1551. return NULL;
  1552. }
  1553. // Get the CV for this term.
  1554. $cv = new ChadoRecord('cv');
  1555. $cv->setValues(['cv_id' => $cvterm->getValue('cv_id')]);
  1556. $cv->find();
  1557. // Create a new stanza using the values of this cvterm.
  1558. $stanza = [];
  1559. $stanza['id'][0] = $short_name . ':' . $accession;
  1560. $stanza['name'][0] = $cvterm->getValue('name');
  1561. $stanza['def'][0] = $cvterm->getValue('definition');
  1562. $stanza['namespace'][0] = $cv->getValue('name');
  1563. $stanza['is_obsolete'][0] = $cvterm->getValue('is_obsolete') == 1 ? 'true' : '';
  1564. $stanza['is_relationshiptype'][0] = '';
  1565. $stanza['db_name'][0] = $db->name;
  1566. $stanza['cv_name'][0] = $cv->getValue('name');
  1567. return $stanza;
  1568. }
  1569. /**
  1570. * Adds a term stanza from the OBO file to the cache for easier lookup.
  1571. *
  1572. * @param $stanza
  1573. * The stanza from the OBO file for the term.
  1574. *
  1575. * @throws Exception
  1576. */
  1577. private function cacheTermStanza($stanza, $type) {
  1578. // Make sure we have defaults.
  1579. if (!$this->default_namespace) {
  1580. throw new Exception('Cannot cache terms without a default CV.' . print_r($stanza, TRUE));
  1581. }
  1582. if (!$this->default_db) {
  1583. throw new Exception('Cannot cache terms without a default DB.' . print_r($stanza, TRUE));
  1584. }
  1585. $id = $stanza['id'][0];
  1586. // First check if this term is already in the cache, if so then skip it.
  1587. if ($this->getCachedTermStanza($id)) {
  1588. return;
  1589. }
  1590. // Does this term have a database short name prefix in the ID (accession)?
  1591. // If not then we'll add the default CV as the namespace. If it does and
  1592. // the short name is not the default for this vocabulary then we'll look
  1593. // it up.
  1594. $matches = [];
  1595. if (preg_match('/^(.+):(.+)$/', $id, $matches)) {
  1596. $short_name = $matches[1];
  1597. $accession = $matches[2];
  1598. // If the term is borrowed then let's try to deal with it.
  1599. $idspaces = array_keys($this->idspaces);
  1600. if ($short_name != $this->default_db and !in_array($short_name, $idspaces)) {
  1601. // First try to lookup the term and replace the stanza with the updated
  1602. // details.
  1603. $found = $this->lookupTerm($short_name, $accession);
  1604. if ($found) {
  1605. $stanza = $found;
  1606. }
  1607. // If we can't find the term in the database then do an EBI lookup.
  1608. else {
  1609. $stanza = $this->findEBITerm($id);
  1610. if (!$stanza) {
  1611. return FALSE;
  1612. }
  1613. // Make sure the DBs and CVs exist and are added to our cache.
  1614. $this->addDB($stanza['db_name'][0]);
  1615. $this->addCV($stanza['namespace'][0]);
  1616. }
  1617. }
  1618. // If the term belongs to this OBO then let's set the 'db_name'.
  1619. else {
  1620. if (!array_key_exists('namespace', $stanza)) {
  1621. $stanza['namespace'][0] = $this->default_namespace;
  1622. }
  1623. $stanza['db_name'][0] = $short_name;
  1624. }
  1625. // Make sure the db for this term is added to Chado. If it already is
  1626. // then this function won't re-add it.
  1627. $this->addDB($short_name);
  1628. }
  1629. // If there is no DB short name prefix for the id.
  1630. else {
  1631. if (!array_key_exists('namespace', $stanza)) {
  1632. $stanza['namespace'][0] = $this->default_namespace;
  1633. }
  1634. $stanza['db_name'][0] = $this->default_db;
  1635. }
  1636. $stanza['is_relationshiptype'][0] = '';
  1637. if ($type == 'Typedef') {
  1638. $stanza['is_relationshiptype'][0] = 'true';
  1639. }
  1640. // The is_a field can have constructs like this: {is_inferred="true"}
  1641. // We need to remove those if they exist.
  1642. if (array_key_exists('is_a', $stanza)) {
  1643. foreach ($stanza['is_a'] as $index => $is_a) {
  1644. $stanza['is_a'][$index] = trim(preg_replace('/\{.+?\}/', '', $is_a));
  1645. }
  1646. }
  1647. if (array_key_exists('relationship', $stanza)) {
  1648. foreach ($stanza['relationship'] as $index => $relationship) {
  1649. $stanza['relationship'][$index] = trim(preg_replace('/\{.+?\}/', '', $relationship));
  1650. }
  1651. }
  1652. // Clean up any synonym definitions. We only handle the synonym in
  1653. // quotes and the type.
  1654. if (array_key_exists('synonym', $stanza)) {
  1655. foreach ($stanza['synonym'] as $index => $synonym) {
  1656. if (preg_match('/\"(.*?)\".*(EXACT|NARROW|BROAD|RELATED)/', $synonym, $matches)) {
  1657. $stanza['synonym'][$index] = '"' . $matches[1] . '" ' . $matches[2];
  1658. }
  1659. }
  1660. }
  1661. // Now before saving, remove any duplicates. Sometimes the OBOs have
  1662. // the same item duplicated in the stanza multiple times. This will
  1663. // result in duplicate constraint violations in the tables. We can either
  1664. // check on every insert if the record exists increasing loading time or
  1665. // remove duplicates here.
  1666. foreach ($stanza as $key => $values) {
  1667. $stanza[$key] = array_unique($values);
  1668. }
  1669. // If we should use the cache_type is to cache in the tripal_obo_temp
  1670. // table then handle that now.
  1671. if ($this->cache_type == 'table') {
  1672. // Add the term to the temp table.
  1673. $values = [
  1674. 'id' => $id,
  1675. 'stanza' => base64_encode(serialize($stanza)),
  1676. 'type' => $type,
  1677. ];
  1678. $success = chado_insert_record('tripal_obo_temp', $values);
  1679. if (!$success) {
  1680. throw new Exception("Cannot insert stanza into temporary table.");
  1681. }
  1682. return;
  1683. }
  1684. // Cache the term stanza
  1685. $this->termStanzaCache['ids'][$id] = $stanza;
  1686. $this->termStanzaCache['count'][$type]++;
  1687. $this->termStanzaCache['types'][$type][] = $id;
  1688. // Cache the term name so we don't have conflicts.
  1689. $name = $stanza['name'][0];
  1690. $this->term_names[$name] = 1;
  1691. }
  1692. /**
  1693. * Returns the size of a given term type from the cache.
  1694. *
  1695. * @param $type
  1696. * The term type: Typedef, Term, etc.
  1697. */
  1698. private function getCacheSize($type) {
  1699. if ($this->cache_type == 'table') {
  1700. $sql = "
  1701. SELECT count(*) as num_terms
  1702. FROM {tripal_obo_temp}
  1703. WHERE type = :type
  1704. ";
  1705. $result = chado_query($sql, [':type' => $type])->fetchObject();
  1706. return $result->num_terms;
  1707. }
  1708. return $this->termStanzaCache['count'][$type];
  1709. }
  1710. /**
  1711. * Retrieves all term IDs for a given type.
  1712. *
  1713. * If the cache is using the tripal_obo_temp table then it
  1714. * returns an iterable Database handle.
  1715. */
  1716. private function getCachedTermStanzas($type) {
  1717. if ($this->cache_type == 'table') {
  1718. $sql = "SELECT id FROM {tripal_obo_temp} WHERE type = 'Typedef' ";
  1719. $typedefs = chado_query($sql);
  1720. return $typedefs;
  1721. }
  1722. return $this->termStanzaCache['types'][$type];
  1723. }
  1724. /**
  1725. * Clear's the term cache.
  1726. */
  1727. private function clearTermStanzaCache() {
  1728. if ($this->cache_type == 'table') {
  1729. $sql = "DELETE FROM {tripal_obo_temp}";
  1730. chado_query($sql);
  1731. return;
  1732. }
  1733. $this->termStanzaCache = [
  1734. 'ids' => [],
  1735. 'count' => [
  1736. 'Typedef' => 0,
  1737. 'Term' => 0,
  1738. 'Instance' => 0,
  1739. ],
  1740. 'types' => [
  1741. 'Typedef' => [],
  1742. 'Term' => [],
  1743. 'Instance' => [],
  1744. ],
  1745. ];
  1746. }
  1747. /**
  1748. * Adds the synonyms to a term
  1749. *
  1750. * @param $cvterm_id
  1751. * The cvterm_id of the term to which the synonym will be added.
  1752. * @param $synonym
  1753. * The value of the 'synonym' line of the term stanza.
  1754. *
  1755. * @ingroup tripal_obo_loader
  1756. */
  1757. private function addSynonym($id, $cvterm_id, $synonym) {
  1758. $def = $synonym;
  1759. $syn_type = '';
  1760. // Separate out the synonym definition and type (e.g. EXACT).
  1761. $matches = [];
  1762. if (preg_match('/\"(.*?)\".*(EXACT|NARROW|BROAD|RELATED)/', $synonym, $matches)) {
  1763. $def = $matches[1];
  1764. $syn_type = strtolower($matches[2]);
  1765. }
  1766. // Get the syn type cvterm.
  1767. if (!$syn_type) {
  1768. $syn_type = 'exact';
  1769. }
  1770. $syn_type_term = $this->syn_types[$syn_type];
  1771. if (!$syn_type_term) {
  1772. throw new Exception(t('Cannot find synonym type: !type', ['!type' => $syn_type]));
  1773. }
  1774. // The synonym can only be 255 chars in the cvtermsynonym table.
  1775. // to prevent failing we have to truncate.
  1776. if (strlen($def) > 255) {
  1777. $def = substr($def, 0, 252) . "...";
  1778. }
  1779. // Now save the new synonym.
  1780. $cvtermsynonym = new ChadoRecord('cvtermsynonym');
  1781. $cvtermsynonym->setValues([
  1782. 'cvterm_id' => $cvterm_id,
  1783. 'synonym' => $def,
  1784. ]);
  1785. // If the insert fails then catch the error and generate a more meaningful
  1786. // message that helps with debugging.
  1787. try {
  1788. // The unique constraint for the cvterm_synonym table is with the
  1789. // cvterm_id and the synonym. It does not include the type_id.
  1790. // The CHEBI contains terms with the same synonym but with different
  1791. // types. For example:
  1792. // synonym: "2-chloro-N-(2-chloroethyl)-N-methylethanamine hydrochloride" EXACT IUPAC_NAME [IUPAC]
  1793. // synonym: "2-chloro-N-(2-chloroethyl)-N-methylethanamine hydrochloride" RELATED [ChemIDplus]
  1794. // In this case on the first one is added.
  1795. // @todo: This is a deficiency with Chado that should be corrected.
  1796. if (!$cvtermsynonym->find()) {
  1797. $cvtermsynonym->setValue('type_id', $syn_type_term->cvterm_id);
  1798. $cvtermsynonym->insert();
  1799. }
  1800. } catch (Exception $e) {
  1801. throw new Exception(t('Cannot add synonym, "!synonym" to term: !id. ' .
  1802. 'ERROR: !error.',
  1803. ['!synonym' => $def, '!id' => $id, '!error' => $e->getMessage()]));
  1804. }
  1805. }
  1806. /**
  1807. * Parse the OBO file and populate the templ loading table
  1808. *
  1809. * @param $file
  1810. * The path on the file system where the ontology can be found
  1811. * @param $header
  1812. * An array passed by reference that will be populated with the header
  1813. * information from the OBO file
  1814. *
  1815. * @ingroup tripal_obo_loader
  1816. */
  1817. private function parse($obo_file, &$header) {
  1818. // Set to 1 if we are in the top header lines of the file.
  1819. $in_header = TRUE;
  1820. // Holds the full stanza for the term.
  1821. $stanza = [];
  1822. // Holds the default database for the term.
  1823. $db_short_name = '';
  1824. $line_num = 0;
  1825. $num_read = 0;
  1826. // The type of term: Typedef or Term (inside the [] brackets]
  1827. $type = '';
  1828. $filesize = filesize($obo_file);
  1829. $this->setTotalItems($filesize);
  1830. $this->setItemsHandled(0);
  1831. $this->setInterval(5);
  1832. // iterate through the lines in the OBO file and parse the stanzas
  1833. $fh = fopen($obo_file, 'r');
  1834. while ($line = fgets($fh)) {
  1835. $line_num++;
  1836. $size = drupal_strlen($line);
  1837. $num_read += $size;
  1838. $line = trim($line);
  1839. $this->setItemsHandled($num_read);
  1840. // remove newlines
  1841. $line = rtrim($line);
  1842. // remove any special characters that may be hiding
  1843. $line = preg_replace('/[^(\x20-\x7F)]*/', '', $line);
  1844. // skip empty lines
  1845. if (strcmp($line, '') == 0) {
  1846. continue;
  1847. }
  1848. // Remove comments from end of lines.
  1849. $line = preg_replace('/^(.*?)\!.*$/', '\1', $line);
  1850. // At the first stanza we're out of header.
  1851. if (preg_match('/^\s*\[/', $line)) {
  1852. // After parsing the header we need to get information about this OBO.
  1853. if ($in_header == TRUE) {
  1854. $this->setDefaults($header);
  1855. $in_header = FALSE;
  1856. }
  1857. // Store the stanza we just finished reading.
  1858. if (sizeof($stanza) > 0) {
  1859. // If this term has a namespace then we want to keep track of it.
  1860. if (array_key_exists('namespace', $stanza)) {
  1861. // Fix the namespace for EDAM terms so they all use the same
  1862. // namespacke (i.e. cv record).
  1863. if ($this->default_namespace == 'EDAM') {
  1864. $stanza['namespace'][0] = 'EDAM';
  1865. }
  1866. $namespace = $stanza['namespace'][0];
  1867. $cv = $this->all_cvs[$namespace];
  1868. $this->obo_namespaces[$namespace] = $cv->cv_id;
  1869. }
  1870. // Before caching this stanza, check the term's name to
  1871. // make sure it doesn't conflict. If it does we'll just
  1872. // add the ID to the name to ensure it doesn't.
  1873. if (array_key_exists($stanza['name'][0], $this->term_names)) {
  1874. $new_name = $stanza['name'][0] . '(' . $stanza['id'][0] . ')';
  1875. $stanza['name'][0] = $new_name;
  1876. }
  1877. $this->cacheTermStanza($stanza, $type);
  1878. }
  1879. // Get the stanza type: Term, Typedef or Instance
  1880. $type = preg_replace('/^\s*\[\s*(.+?)\s*\]\s*$/', '\1', $line);
  1881. // start fresh with a new array
  1882. $stanza = [];
  1883. continue;
  1884. }
  1885. // For EDAM, we have to unfortuantely hard-code a fix as the
  1886. // short names of terms are correct.
  1887. $line = preg_replace('/EDAM_(\w+)/', '\1', $line);
  1888. // break apart the line into the tag and value but ignore any escaped colons
  1889. preg_replace("/\\:/", "|-|-|", $line); // temporarily replace escaped colons
  1890. $pair = explode(":", $line, 2);
  1891. $tag = $pair[0];
  1892. $value = ltrim(rtrim($pair[1]));// remove surrounding spaces
  1893. // if this is the ID line then get the database short name from the ID.
  1894. $matches = [];
  1895. if ($tag == 'id' and preg_match('/^(.+?):.*$/', $value, $matches)) {
  1896. $db_short_name = $matches[1];
  1897. }
  1898. $tag = preg_replace("/\|-\|-\|/", "\:", $tag); // return the escaped colon
  1899. $value = preg_replace("/\|-\|-\|/", "\:", $value);
  1900. if ($in_header) {
  1901. if (!array_key_exists($tag, $header)) {
  1902. $header[$tag] = [];
  1903. }
  1904. $header[$tag][] = $value;
  1905. }
  1906. else {
  1907. if (!array_key_exists($tag, $stanza)) {
  1908. $stanza[$tag] = [];
  1909. }
  1910. $stanza[$tag][] = $value;
  1911. }
  1912. }
  1913. // now add the last term in the file
  1914. if (sizeof($stanza) > 0) {
  1915. // If this term has a namespace then we want to keep track of it.
  1916. if (array_key_exists('namespace', $stanza)) {
  1917. $namespace = $stanza['namespace'][0];
  1918. $cv = $this->all_cvs[$namespace];
  1919. $this->obo_namespaces[$namespace] = $cv->cv_id;
  1920. }
  1921. $this->cacheTermStanza($stanza, $type);
  1922. $this->setItemsHandled($num_read);
  1923. }
  1924. // Make sure there are CV records for all namespaces.
  1925. $message = t('Found the following namespaces: !namespaces.',
  1926. ['!namespaces' => implode(', ', array_keys($this->obo_namespaces))]);
  1927. foreach ($this->obo_namespaces as $namespace => $cv_id) {
  1928. $this->addCV($namespace);
  1929. }
  1930. $this->logMessage($message);
  1931. }
  1932. /**
  1933. * Iterates through all of the cached terms and caches any relationships
  1934. */
  1935. private function cacheRelationships() {
  1936. // Now that we have all of the terms parsed and loaded into the cache,
  1937. // lets run through them one more time cache any terms in relationships
  1938. // as well.
  1939. $terms = $this->getCachedTermStanzas('Term');
  1940. $count = $this->getCacheSize('Term');
  1941. $this->setTotalItems($count);
  1942. $this->setItemsHandled(0);
  1943. $this->setInterval(25);
  1944. // Iterate through the terms.
  1945. $i = 1;
  1946. foreach ($terms as $t) {
  1947. // TODO: it would be better if we had a term iterator so that we
  1948. // don't have to distinguish here between the table vs memory cache type.
  1949. if ($this->cache_type == 'table') {
  1950. $stanza = unserialize(base64_decode($t->stanza));
  1951. }
  1952. else {
  1953. $stanza = $this->termStanzaCache['ids'][$t];
  1954. }
  1955. // Check if this stanza has an is_a relationship that needs lookup.
  1956. if (array_key_exists('is_a', $stanza)) {
  1957. foreach ($stanza['is_a'] as $object_term) {
  1958. $rstanza = [];
  1959. $rstanza['id'][] = $object_term;
  1960. $this->cacheTermStanza($rstanza, 'Term');
  1961. }
  1962. }
  1963. // Check if this stanza has any additional relationships for lookup.
  1964. if (array_key_exists('relationship', $stanza)) {
  1965. foreach ($stanza['relationship'] as $value) {
  1966. // Get the relationship term and the object term
  1967. $rel_term = preg_replace('/^(.+?)\s.+?$/', '\1', $value);
  1968. $object_term = preg_replace('/^.+?\s(.+?)$/', '\1', $value);
  1969. $rstanza = [];
  1970. $rstanza['id'][] = $rel_term;
  1971. $this->cacheTermStanza($rstanza, 'Typedef');
  1972. $rstanza = [];
  1973. $rstanza['id'][] = $object_term;
  1974. $this->cacheTermStanza($rstanza, 'Term');
  1975. }
  1976. }
  1977. }
  1978. $this->setItemsHandled($i++);
  1979. // Last of all, we need to add the "is_a" relationship It's part of the
  1980. // OBO specification as a built-in relationship but not all vocabularies
  1981. // include that term.
  1982. if (!$this->getCachedTermStanza('is_a')) {
  1983. $stanza = [];
  1984. $stanza['id'][0] = 'is_a';
  1985. $stanza['name'][0] = 'is_a';
  1986. $stanza['namespace'][0] = $this->default_namespace;
  1987. $stanza['db_name'][0] = $this->default_db;
  1988. $this->cacheTermStanza($stanza, 'Typedef');
  1989. }
  1990. }
  1991. /**
  1992. * Adds a property to the cvterm indicating it belongs to a subset.
  1993. *
  1994. * @param $cvterm_id
  1995. * The cvterm_id of the term to which the subset will be added.
  1996. * @param $subset
  1997. * The name of the subset.
  1998. */
  1999. private function addSubset($id, $cvterm_id, $subset) {
  2000. $cvtermprop = new ChadoRecord('cvtermprop');
  2001. $cvtermprop->setValues([
  2002. 'cvterm_id' => $cvterm_id,
  2003. 'type_id' => $this->used_terms['NCIT:C25693'],
  2004. 'value' => $subset,
  2005. ]);
  2006. // If the insert fails then catch the error and generate a more meaningful
  2007. // message that helps with debugging.
  2008. try {
  2009. $cvtermprop->insert();
  2010. } catch (Exception $e) {
  2011. throw new Exception(t('Cannot add subset, "!subset" to term: !id. ' .
  2012. 'ERROR: !error.',
  2013. ['!subset' => $subset, '!id' => $id, '!error' => $e->getMessage()]));
  2014. }
  2015. }
  2016. /**
  2017. * Adds a database to Chado if it doesn't exist.
  2018. *
  2019. * @param $dbname
  2020. * The name of the database to add.
  2021. *
  2022. * @return
  2023. * A Chado database object.
  2024. */
  2025. private function addDB($dbname, $url = '', $description = '') {
  2026. // Add the database if it doesn't exist.
  2027. $db = NULL;
  2028. if (array_key_exists($dbname, $this->all_dbs)) {
  2029. $db = $this->all_dbs[$dbname];
  2030. }
  2031. else {
  2032. // If it's not in the cache we can assume it doesn't exist and insert.
  2033. $db = new ChadoRecord('db');
  2034. $values = ['name' => $dbname];
  2035. if ($url) {
  2036. $values['url'] = $url;
  2037. }
  2038. if ($description) {
  2039. $values['description'] = $description;
  2040. }
  2041. $db->setValues($values);
  2042. $db->insert();
  2043. $db = (object) $db->getValues();
  2044. $this->all_dbs[$dbname] = $db;
  2045. }
  2046. return $db;
  2047. }
  2048. /**
  2049. * Adds a vocabulary to Chado if it doesn't exist.
  2050. *
  2051. * @param $cvname
  2052. * The name of the vocabulary to add.
  2053. *
  2054. * @return
  2055. * A Chado cv object.
  2056. */
  2057. private function addCV($cvname) {
  2058. // TODO: we need to get the description and title from EBI for these
  2059. // ontology so that we can put something in the proper fields when
  2060. // adding a new CV or DB.
  2061. // Add the CV record if it doesn't exist.
  2062. $cv = NULL;
  2063. if (array_key_exists($cvname, $this->all_cvs)) {
  2064. $cv = $this->all_cvs[$cvname];
  2065. }
  2066. else {
  2067. // If it's not in the cache we can assume it doesn't exist and insert.
  2068. $cv = new ChadoRecord('cv');
  2069. $cv->setValues(['name' => $cvname]);
  2070. $cv->insert();
  2071. $cv = (object) $cv->getValues();
  2072. $this->all_cvs[$cvname] = $cv;
  2073. $this->obo_namespaces[$cvname] = $cv->cv_id;
  2074. }
  2075. return $cv;
  2076. }
  2077. /**
  2078. * Indicates if the term belongs to this OBO or if it was borrowed
  2079. * .
  2080. *
  2081. * @param $stanza
  2082. */
  2083. private function isTermBorrowed($stanza) {
  2084. $namespace = $stanza['namespace'][0];
  2085. if (array_key_exists($namespace, $this->obo_namespaces)) {
  2086. return FALSE;
  2087. }
  2088. return TRUE;
  2089. }
  2090. /**
  2091. * Adds an alternative ID
  2092. *
  2093. * @param $cvterm_id
  2094. * The cvterm_id of the term to which the synonym will be added.
  2095. * @param $alt_id
  2096. * The cross reference. It should be of the form from the OBO specification
  2097. *
  2098. * @ingroup tripal_obo_loader
  2099. */
  2100. private function addAltID($id, $cvterm_id, $alt_id) {
  2101. $dbname = '';
  2102. $accession = '';
  2103. $matches = [];
  2104. if (preg_match('/^(.+?):(.*)$/', $alt_id, $matches)) {
  2105. $dbname = $matches[1];
  2106. $accession = $matches[2];
  2107. }
  2108. if (!$accession) {
  2109. $this->logMessage("Cannot add an Alt ID without an accession: '!alt_id'", ['!alt_id' => $alt_id]);
  2110. return;
  2111. }
  2112. // Add the database if it doesn't exist.
  2113. $db = $this->addDB($dbname);
  2114. $db_id = $db->db_id;
  2115. // Now add the dbxref if it doesn't already exist
  2116. $dbxref = new ChadoRecord('dbxref');
  2117. $dbxref->setValues([
  2118. 'db_id' => $db_id,
  2119. 'accession' => $accession,
  2120. ]);
  2121. if (!$dbxref->find()) {
  2122. $dbxref->insert();
  2123. }
  2124. // Now add the cvterm_dbxref record.
  2125. $cvterm_dbxref = new ChadoRecord('cvterm_dbxref');
  2126. $cvterm_dbxref->setValues([
  2127. 'cvterm_id' => $cvterm_id,
  2128. 'dbxref_id' => $dbxref->getID(),
  2129. ]);
  2130. if (!$cvterm_dbxref->find()) {
  2131. $cvterm_dbxref->insert();
  2132. }
  2133. }
  2134. /**
  2135. * Adds a database reference to a cvterm
  2136. *
  2137. * @param $cvterm_id
  2138. * The cvterm_id of the term to which the synonym will be added.
  2139. * @param xref
  2140. * The cross reference. It should be of the form from the OBO specification
  2141. *
  2142. * @ingroup tripal_obo_loader
  2143. */
  2144. private function addXref($id, $cvterm_id, $xref) {
  2145. $dbname = preg_replace('/^(.+?):.*$/', '$1', $xref);
  2146. $accession = preg_replace('/^.+?:\s*(.*?)(\{.+$|\[.+$|\s.+$|\".+$|$)/', '$1', $xref);
  2147. $description = preg_replace('/^.+?\"(.+?)\".*?$/', '$1', $xref);
  2148. $dbxrefs = preg_replace('/^.+?\[(.+?)\].*?$/', '$1', $xref);
  2149. if (!$accession) {
  2150. throw new Exception("Cannot add an xref without an accession: '$xref'");
  2151. }
  2152. // If the xref is a database link then skip those for now.
  2153. if (strcmp($dbname, 'http') == 0) {
  2154. return;
  2155. }
  2156. // Add the database if it doesn't exist.
  2157. $db = $this->addDB($dbname);
  2158. $db_id = $db->db_id;
  2159. // Now add the dbxref if it doesn't already exist
  2160. $dbxref = new ChadoRecord('dbxref');
  2161. $dbxref->setValues([
  2162. 'db_id' => $db_id,
  2163. 'accession' => $accession,
  2164. ]);
  2165. if (!$dbxref->find()) {
  2166. $dbxref->insert();
  2167. }
  2168. // Now add the cvterm_dbxref record.
  2169. $cvterm_dbxref = new ChadoRecord('cvterm_dbxref');
  2170. $cvterm_dbxref->setValues([
  2171. 'cvterm_id' => $cvterm_id,
  2172. 'dbxref_id' => $dbxref->getID(),
  2173. ]);
  2174. if (!$cvterm_dbxref->find()) {
  2175. $cvterm_dbxref->insert();
  2176. }
  2177. }
  2178. /**
  2179. * Adds a comment to a cvterm.
  2180. *
  2181. * @param $cvterm_id
  2182. * A cvterm_id of the term to which properties will be added
  2183. * @param $comment
  2184. * The comment to add to the cvterm.
  2185. * @param rank
  2186. * The rank of the comment
  2187. *
  2188. * @ingroup tripal_obo_loader
  2189. */
  2190. private function addComment($id, $cvterm_id, $comment, $rank) {
  2191. // Get the comment type id.
  2192. $comment_type_id = $this->used_terms['rdfs:comment'];
  2193. // Add the comment as a property of the cvterm.
  2194. $cvtermprop = new ChadoRecord('cvtermprop');
  2195. $cvtermprop->setValues([
  2196. 'cvterm_id' => $cvterm_id,
  2197. 'type_id' => $comment_type_id,
  2198. 'value' => $comment,
  2199. 'rank' => $rank,
  2200. ]);
  2201. // If the insert fails then catch the error and generate a more meaningful
  2202. // message that helps with debugging.
  2203. try {
  2204. $cvtermprop->insert();
  2205. } catch (Exception $e) {
  2206. throw new Exception(t('Cannot add comment, "!comment" to term: !id. ' .
  2207. 'ERROR: !error.',
  2208. ['!comment' => $comment, '!id' => $id, '!error' => $e->getMessage()]));
  2209. }
  2210. }
  2211. /**
  2212. * API call to Ontology Lookup Service provided by
  2213. * https://www.ebi.ac.uk/ols/docs/api#resources-terms
  2214. *
  2215. * @param accession
  2216. * Accession term for query
  2217. * @param type_of_search
  2218. * Either ontology, term, query, or query-non-local
  2219. *
  2220. * @ingroup tripal_obo_loader
  2221. */
  2222. private function oboEbiLookup($accession, $type_of_search) {
  2223. //Grab just the ontology from the $accession.
  2224. $parts = explode(':', $accession);
  2225. $ontology = strtolower($parts[0]);
  2226. $ontology = preg_replace('/\s+/', '', $ontology);
  2227. if ($type_of_search == 'ontology') {
  2228. $options = [];
  2229. $full_url = 'http://www.ebi.ac.uk/ols/api/ontologies/' . $ontology;
  2230. $response = drupal_http_request($full_url, $options);
  2231. if (!empty($response)) {
  2232. $response = drupal_json_decode($response->data);
  2233. }
  2234. }
  2235. elseif ($type_of_search == 'term') {
  2236. //The IRI of the terms, this value must be double URL encoded
  2237. $iri = urlencode(urlencode("http://purl.obolibrary.org/obo/" . str_replace(':', '_', $accession)));
  2238. $options = [];
  2239. $full_url = 'http://www.ebi.ac.uk/ols/api/ontologies/' . $ontology . '/' . 'terms/' . $iri;
  2240. $response = drupal_http_request($full_url, $options);
  2241. if (!empty($response)) {
  2242. $response = drupal_json_decode($response->data);
  2243. }
  2244. }
  2245. elseif ($type_of_search == 'query') {
  2246. $options = [];
  2247. $full_url = 'http://www.ebi.ac.uk/ols/api/search?q=' . $accession . '&queryFields=obo_id&local=true';
  2248. $response = drupal_http_request($full_url, $options);
  2249. if (!empty($response)) {
  2250. $response = drupal_json_decode($response->data);
  2251. }
  2252. }
  2253. elseif ($type_of_search == 'query-non-local') {
  2254. $options = [];
  2255. $full_url = 'http://www.ebi.ac.uk/ols/api/search?q=' . $accession . '&queryFields=obo_id';
  2256. $response = drupal_http_request($full_url, $options);
  2257. if (!empty($response)) {
  2258. $response = drupal_json_decode($response->data);
  2259. }
  2260. }
  2261. return $response;
  2262. }
  2263. }
  2264. /**
  2265. * Ajax callback for the OBOImporter::form() function.
  2266. */
  2267. function tripal_cv_obo_form_ajax_callback($form, $form_state) {
  2268. return $form['obo_existing'];
  2269. }