]> git.donarmstrong.com Git - org-ref.git/blob - doi-utils.el
shortened description
[org-ref.git] / doi-utils.el
1 ;;; doi-utils.el --- DOI utilities for making bibtex entries
2
3 ;; Copyright (C) 2015  John Kitchin
4
5 ;; Author: John Kitchin <jkitchin@andrew.cmu.edu>
6 ;; Keywords: convenience
7 ;; Version: 0.1
8 ;; Package-Requires: ((org-ref))
9
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This package provides functionality to download PDFs and bibtex entries from a DOI, as well as to update a bibtex entry from a DOI. It depends slightly on org-ref, to determine where to save pdf files too, and where to insert bibtex entries in the default bibliography.
26
27 ;; The principle commands you will use from here are:
28
29 ;; - doi-utils-get-bibtex-entry-pdf with the cursor in a bibtex entry.
30 ;; - doi-utils-insert-bibtex-entry-from-doi to insert a bibtex entry at your cursor, clean it and try to get a pdf.
31 ;; - doi-utils-add-bibtex-entry-from-doi to add an entry to your default bibliography (cleaned with pdf if possible).
32 ;; - doi-utils-add-bibtex-entry-from-region to add an entry from a highlighed doi to your default bibliography.
33 ;; - doi-utils-update-bibtex-entry-from-doi with cursor in an entry to update its fields.
34
35 (require 'json)
36
37 ;;; Code:
38 ;; * Getting pdf files from a DOI
39 ;; The idea here is simple. When you visit http://dx.doi.org/doi, you get redirected to the journal site. Once you have the url for the article, you can usually compute the url to the pdf, or find it in the page. Then you simply download it.
40
41 ;; There are some subtleties in doing this that are described here. To get the redirect, we have to use url-retrieve, and a callback function. The callback does not return anything, so we communicate through global variables. url-retrieve is asynchronous, so we have to make sure to wait for it to finish.
42
43 (defvar *doi-utils-waiting* t
44   "stores waiting state for url retrieval.")
45
46 (defvar *doi-utils-redirect* nil
47   "stores redirect url from a callback function")
48
49 (defun doi-utils-redirect-callback (&optional status)
50   "callback for url-retrieve to set the redirect"
51   (when (plist-get status :error)
52     (signal (car (plist-get status :error)) (cdr(plist-get status :error))))
53   (when (plist-get status :redirect) ;  is nil if there none
54     (message "redirects = %s" (plist-get status :redirect))
55     (message "*doi-utils-redirect* set to %s"
56              (setq *doi-utils-redirect* (plist-get status :redirect))))
57   ;; we have done our job, so we are not waiting any more.
58   (setq *doi-utils-waiting* nil))
59
60 ;; To actually get the redirect we use url-retrieve like this.
61
62 (defun doi-utils-get-redirect (doi)
63   "get redirect url from dx.doi.org/doi"
64   ;; we are going to wait until the url-retrieve is done
65   (setq *doi-utils-waiting* t)
66   ;; start with no redirect. it will be set in the callback.
67   (setq *doi-utils-redirect* nil)
68   (url-retrieve
69    (format "http://dx.doi.org/%s" doi)
70    'doi-utils-redirect-callback)
71   ; I suspect we need to wait here for the asynchronous process to
72   ; finish. we loop and sleep until the callback says it is done via
73   ; `*doi-utils-waiting*'. this works as far as i can tell. Before I
74   ; had to run this a few times to get it to work, which i suspect
75   ; just gave the first one enough time to finish.
76   (while *doi-utils-waiting* (sleep-for 0.1)))
77
78 ;; Once we have a redirect for a particular doi, we need to compute the url to the pdf. We do this with a series of functions. Each function takes a single argument, the redirect url. If it knows how to compute the pdf url it does, and returns it. We store the functions in a variable:
79
80 (defvar doi-utils-pdf-url-functions nil
81   "list of functions that return a url to a pdf from a redirect url. Each function takes one argument, the redirect url. The function must return a pdf-url, or nil.")
82
83
84 ;; ** APS journals
85
86 (defun aps-pdf-url (*doi-utils-redirect*)
87   (when (string-match "^http://journals.aps.org" *doi-utils-redirect*)
88     (replace-regexp-in-string "/abstract/" "/pdf/" *doi-utils-redirect*)))
89
90
91 ;; ** Science
92
93 (defun science-pdf-url (*doi-utils-redirect*)
94   (when (string-match "^http://www.sciencemag.org" *doi-utils-redirect*)
95     (concat *doi-utils-redirect* ".full.pdf")))
96
97
98 ;; ** Nature
99
100 (defun nature-pdf-url (*doi-utils-redirect*)
101   (when (string-match "^http://www.nature.com" *doi-utils-redirect*)
102     (let ((result *doi-utils-redirect*))
103       (setq result (replace-regexp-in-string "/full/" "/pdf/" result))
104       (replace-regexp-in-string "\.html$" "\.pdf" result))))
105
106
107 ;; ** Wiley
108 ;; http://onlinelibrary.wiley.com/doi/10.1002/anie.201402680/abstract
109 ;; http://onlinelibrary.wiley.com/doi/10.1002/anie.201402680/pdf
110
111 ;; It appears that it is not enough to use the pdf url above. That takes you to an html page. The actual link to teh pdf is embedded in that page. This is how ScienceDirect does things too.
112
113 ;; This is where the link is hidden:
114
115 ;; <iframe id="pdfDocument" src="http://onlinelibrary.wiley.com/store/10.1002/anie.201402680/asset/6397_ftp.pdf?v=1&amp;t=hwut2142&amp;s=d4bb3cd4ad20eb733836717f42346ffb34017831" width="100%" height="675px"></iframe>
116
117
118 (defun doi-utils-get-wiley-pdf-url (redirect-url)
119   "wileyscience direct hides the pdf url in html. we get it out here"
120   (setq *doi-utils-waiting* t)
121   (url-retrieve redirect-url
122                 (lambda (status)
123                   (beginning-of-buffer)
124                   (re-search-forward "<iframe id=\"pdfDocument\" src=\"\\([^\"]*\\)\"" nil)
125                   (setq *doi-utils-pdf-url* (match-string 1)
126                         ,*doi-utils-waiting* nil)))
127   (while *doi-utils-waiting* (sleep-for 0.1))
128   *doi-utils-pdf-url*)
129
130 (defun wiley-pdf-url (*doi-utils-redirect*)
131   (when (string-match "^http://onlinelibrary.wiley.com" *doi-utils-redirect*)
132     (doi-utils-get-wiley-pdf-url
133      (replace-regexp-in-string "/abstract" "/pdf" *doi-utils-redirect*))
134    *doi-utils-pdf-url*))
135
136
137 ;; ** Springer
138
139 (defun springer-pdf-url (*doi-utils-redirect*)
140   (when (string-match "^http://link.springer.com" *doi-utils-redirect*)
141     (replace-regexp-in-string "/article/" "/content/pdf/" (concat *doi-utils-redirect* ".pdf"))))
142
143
144 ;; ** ACS
145 ;; here is a typical url http://pubs.acs.org/doi/abs/10.1021/nl500037x
146 ;; the pdf is found at http://pubs.acs.org/doi/pdf/10.1021/nl500037x
147
148 ;; we just change /abs/ to /pdf/.
149
150 (defun acs-pdf-url (*doi-utils-redirect*)
151   (when (string-match "^http://pubs.acs.org" *doi-utils-redirect*)
152     (replace-regexp-in-string "/abs/" "/pdf/" *doi-utils-redirect*)))
153
154
155 ;; ** IOP
156
157 (defun iop-pdf-url (*doi-utils-redirect*)
158   (when (string-match "^http://iopscience.iop.org" *doi-utils-redirect*)
159     (let ((tail (replace-regexp-in-string
160                  "^http://iopscience.iop.org" "" *doi-utils-redirect*)))
161       (concat "http://iopscience.iop.org" tail
162               "/pdf" (replace-regexp-in-string "/" "_" tail) ".pdf"))))
163
164
165 ;; ** JSTOR
166
167 (defun jstor-pdf-url (*doi-utils-redirect*)
168   (when (string-match "^http://www.jstor.org" *doi-utils-redirect*)
169     (concat (replace-regexp-in-string "/stable/" "/stable/pdfplus/" *doi-utils-redirect*) ".pdf")))
170
171
172 ;; ** AIP
173
174 (defun aip-pdf-url (*doi-utils-redirect*)
175   (when (string-match "^http://scitation.aip.org" *doi-utils-redirect*)
176     ;; get stuff after content
177     (let (p1 p2 s p3)
178       (setq p2 (replace-regexp-in-string "^http://scitation.aip.org/" "" *doi-utils-redirect*))
179       (setq s (split-string p2 "/"))
180       (setq p1 (mapconcat 'identity (-remove-at-indices '(0 6) s) "/"))
181       (setq p3 (concat "/" (nth 0 s) (nth 1 s) "/" (nth 2 s) "/" (nth 3 s)))
182       (format "http://scitation.aip.org/deliver/fulltext/%s.pdf?itemId=/%s&mimeType=pdf&containerItemId=%s"
183               p1 p2 p3))))
184
185 ;; ** Taylor and Francis
186
187 (defun tandfonline-pdf-url (*doi-utils-redirect*)
188   (when (string-match "^http://www.tandfonline.com" *doi-utils-redirect*)
189     (replace-regexp-in-string "/abs/\\|/full/" "/pdf/" *doi-utils-redirect*)))
190
191 ;; ** ECS
192
193 (defun ecs-pdf-url (*doi-utils-redirect*)
194   (when (string-match "^http://jes.ecsdl.org" *doi-utils-redirect*)
195     (replace-regexp-in-string "\.abstract$" ".full.pdf" *doi-utils-redirect*)))
196
197 ;; http://ecst.ecsdl.org/content/25/2/2769
198 ;; http://ecst.ecsdl.org/content/25/2/2769.full.pdf
199
200
201 (defun ecst-pdf-url (*doi-utils-redirect*)
202   (when (string-match "^http://ecst.ecsdl.org" *doi-utils-redirect*)
203     (concat *doi-utils-redirect* ".full.pdf")))
204
205
206
207 ;; ** RSC
208
209 (defun rsc-pdf-url (*doi-utils-redirect*)
210   (when (string-match "^http://pubs.rsc.org" *doi-utils-redirect*)
211     (let ((url (downcase *doi-utils-redirect*)))
212       (setq url (replace-regexp-in-string "articlelanding" "articlepdf" url))
213       url)))
214
215 ;; ** Elsevier/ScienceDirect
216 ;; You cannot compute these pdf links; they are embedded in the redirected pages.
217
218 (defvar *doi-utils-pdf-url* nil
219   "stores url to pdf download from a callback function")
220
221 (defun doi-utils-get-science-direct-pdf-url (redirect-url)
222   "science direct hides the pdf url in html. we get it out here"
223   (setq *doi-utils-waiting* t)
224   (url-retrieve redirect-url
225                 (lambda (status)
226                   (beginning-of-buffer)
227                   (re-search-forward "pdfurl=\"\\([^\"]*\\)\"" nil t)
228                   (setq *doi-utils-pdf-url* (match-string 1)
229                         ,*doi-utils-waiting* nil)))
230   (while *doi-utils-waiting* (sleep-for 0.1))
231   *doi-utils-pdf-url*)
232
233
234 (defun science-direct-pdf-url (*doi-utils-redirect*)
235   (when (string-match "^http://www.sciencedirect.com" *doi-utils-redirect*)
236     (doi-utils-get-science-direct-pdf-url *doi-utils-redirect*)
237     *doi-utils-pdf-url*))
238
239 ;; sometimes I get
240 ;; http://linkinghub.elsevier.com/retrieve/pii/S0927025609004558
241 ;; which actually redirect to
242 ;; http://www.sciencedirect.com/science/article/pii/S0927025609004558
243 (defun linkinghub-elsevier-pdf-url (*doi-utils-redirect*)
244   (when (string-match "^http://linkinghub.elsevier.com/retrieve" *doi-utils-redirect*)
245     (let ((second-redirect (replace-regexp-in-string
246                             "http://linkinghub.elsevier.com/retrieve"
247                             "http://www.sciencedirect.com/science/article"
248                             ,*doi-utils-redirect*)))
249       (message "getting pdf url from %s" second-redirect)
250       *doi-utils-pdf-url*)))
251
252 ;; ** PNAS
253 ;; http://www.pnas.org/content/early/2014/05/08/1319030111
254 ;; http://www.pnas.org/content/early/2014/05/08/1319030111.full.pdf
255
256 ;; with supporting info
257 ;; http://www.pnas.org/content/early/2014/05/08/1319030111.full.pdf+html?with-ds=yes
258
259 (defun pnas-pdf-url (*doi-utils-redirect*)
260   (when (string-match "^http://www.pnas.org" *doi-utils-redirect*)
261     (concat *doi-utils-redirect* ".full.pdf?with-ds=yes")))
262
263
264 ;; ** Add all functions
265
266 (setq doi-utils-pdf-url-functions
267       (list
268        'aps-pdf-url
269        'science-pdf-url
270        'nature-pdf-url
271        'wiley-pdf-url
272        'springer-pdf-url
273        'acs-pdf-url
274        'iop-pdf-url
275        'jstor-pdf-url
276        'aip-pdf-url
277        'science-direct-pdf-url
278        'linkinghub-elsevier-pdf-url
279        'tandfonline-pdf-url
280        'ecs-pdf-url
281        'ecst-pdf-url
282        'rsc-pdf-url
283        'pnas-pdf-url))
284
285 ;; ** Get the pdf url for a doi
286
287 (defun doi-utils-get-pdf-url (doi)
288   "returns a url to a pdf for the doi if one can be
289 calculated. Loops through the functions in `doi-utils-pdf-url-functions'
290 until one is found"
291   (doi-utils-get-redirect doi)
292
293   (unless *doi-utils-redirect*
294     (error "No redirect found for %s" doi))
295   (message "applying functions")
296   (catch 'pdf-url
297     (dolist (func doi-utils-pdf-url-functions)
298      (message "calling %s" func)
299       (let ((this-pdf-url (funcall func *doi-utils-redirect*)))
300 (message "t: %s" this-pdf-url)
301         (when this-pdf-url
302           (message "found pdf url: %s" this-pdf-url)
303           (throw 'pdf-url this-pdf-url))))))
304
305 ;; ** Finally, download the pdf
306
307 (defun doi-utils-get-bibtex-entry-pdf ()
308   "download pdf for entry at point if the pdf does not already
309 exist locally. The entry must have a doi. The pdf will be saved
310 to `org-ref-pdf-directory', by the name %s.pdf where %s is the
311 bibtex label. Files will not be overwritten. The pdf will be
312 checked to make sure it is a pdf, and not some html failure
313 page. you must have permission to access the pdf. We open the pdf
314 at the end."
315   (interactive)
316   (save-excursion
317     (bibtex-beginning-of-entry)
318     (let (;; get doi, removing http://dx.doi.org/ if it is there.
319           (doi (replace-regexp-in-string
320                 "http://dx.doi.org/" ""
321                 (bibtex-autokey-get-field "doi")))
322           (key)
323           (pdf-url)
324           (pdf-file)
325           (content))
326       ;; get the key and build pdf filename.
327       (re-search-forward bibtex-entry-maybe-empty-head)
328       (setq key (match-string bibtex-key-in-head))
329       (setq pdf-file (concat org-ref-pdf-directory key ".pdf"))
330
331       ;; now get file if needed.
332       (when (and doi (not (file-exists-p pdf-file)))
333         (setq pdf-url (doi-utils-get-pdf-url doi))
334         (if pdf-url
335             (progn
336               (url-copy-file pdf-url pdf-file)
337               ;; now check if we got a pdf
338               (with-temp-buffer
339                 (insert-file-contents pdf-file)
340                 ;; PDFS start with %PDF-1.x as the first few characters.
341                 (if (not (string= (buffer-substring 1 6) "%PDF-"))
342                     (progn
343                       (message "%s" (buffer-string))
344                       (delete-file pdf-file))
345                   (message "%s saved" pdf-file)))
346
347               (when (file-exists-p pdf-file)
348                 (org-open-file pdf-file)))
349           (message "No pdf-url found for %s at %s" doi *doi-utils-redirect* ))
350           pdf-file))))
351
352 ;; * Getting bibtex entries from a DOI
353
354 ;; I [[http://homepages.see.leeds.ac.uk/~eeaol/notes/2013/02/doi-metadata/][found]] you can download metadata about a DOI from http://dx.doi.org. You just have to construct the right http request to get it. Here is a function that gets the metadata as a plist in emacs.
355
356 (defun doi-utils-get-json-metadata (doi)
357   "Try to get json metadata for DOI. Open the DOI in a browser if we do not get it."
358   (let ((url-request-method "GET")
359         (url-mime-accept-string "application/citeproc+json")
360         (json-object-type 'plist)
361         (json-data))
362     (with-current-buffer
363         (url-retrieve-synchronously
364          (concat "http://dx.doi.org/" doi))
365       (setq json-data (buffer-substring url-http-end-of-headers (point-max)))
366       (if (string-match "Resource not found" json-data)
367           (progn
368             (browse-url (concat "http://dx.doi.org/" doi))
369             (error "Resource not found. Opening website."))
370         (json-read-from-string json-data)))))
371
372 ;; We can use that data to construct a bibtex entry. We do that by defining a template, and filling it in. I wrote this template expansion code which makes it easy to substitute values like %{} in emacs lisp.
373
374
375 (defun doi-utils-expand-template (s)
376   "expand a template containing %{} with the eval of its contents"
377   (replace-regexp-in-string "%{\\([^}]+\\)}"
378                             (lambda (arg)
379                               (let ((sexp (substring arg 2 -1)))
380                                 (format "%s" (eval (read sexp))))) s))
381
382
383 ;; Now we define a function that fills in that template from the metadata.
384
385 ;; As different bibtex types share common keys, it is advantageous to separate data extraction from json, and the formatting of the bibtex entry.
386
387
388 (setq doi-utils-json-metadata-extract
389       '((type       (plist-get results :type))
390         (author     (mapconcat (lambda (x) (concat (plist-get x :given) " " (plist-get x :family)))
391                      (plist-get results :author) " and "))
392         (title      (plist-get results :title))
393         (subtitle   (plist-get results :subtitle))
394         (journal    (plist-get results :container-title))
395         (series     (plist-get results :container-title))
396         (publisher  (plist-get results :publisher))
397         (volume     (plist-get results :volume))
398         (issue      (plist-get results :issue))
399         (number     (plist-get results :issue))
400         (year       (elt (elt (plist-get (plist-get results :issued) :date-parts) 0) 0))
401         (month      (elt (elt (plist-get (plist-get results :issued) :date-parts) 0) 1))
402         (pages      (plist-get results :page))
403         (doi        (plist-get results :DOI))
404         (url        (plist-get results :URL))
405         (booktitle  (plist-get results :container-title))))
406
407 ;; Next, we need to define the different bibtex types. Each type has a bibtex type (for output) and the type as provided in the doi record. Finally, we have to declare the fields we want to output.
408
409 (setq doi-utils-bibtex-type-generators nil)
410
411 (defun doi-utils-concat-prepare (lst &optional acc)
412   "Given a list `lst' of strings and other expressions, which are
413 intented to passed to `concat', concat any subsequent strings,
414 minimising the number of arguments being passed to `concat'
415 without changing the results."
416   (cond ((null lst) (nreverse acc))
417         ((and (stringp (car lst))
418               (stringp (car acc)))
419          (doi-utils-concat-prepare (cdr lst) (cons (concat (car acc) (car lst))
420                                          (cdr acc))))
421         (t (doi-utils-concat-prepare (cdr lst) (cons (car lst) acc)))))
422
423 (defmacro doi-utils-def-bibtex-type (name matching-types &rest fields)
424   "Define a BibTeX type identified by (symbol) `name' with
425 `fields' (given as symbols), matching to retrieval expressions in
426 `doi-utils-json-metadata-extract'. This type will only be used
427 when the `:type' parameter in the JSON metadata is contained in
428 `matching-types' - a list of strings."
429   `(push (lambda (type results)
430            (when (or ,@(mapcar (lambda (match-type) `(string= type ,match-type)) matching-types))
431              (let ,(mapcar (lambda (field)
432                              (let ((field-expr (assoc field doi-utils-json-metadata-extract)))
433                                (if field-expr
434                                    ;; need to convert to string first
435                                    `(,(car field-expr) (format "%s" ,(cadr field-expr)))
436                                    (error "unknown bibtex field type %s" field))))
437                            fields)
438                (concat
439                 ,@(doi-utils-concat-prepare
440                    (-flatten
441                     (list (concat "@" (symbol-name name) "{,\n")
442                           ;; there seems to be some bug with mapcan,
443                           ;; so we fall back to flatten
444                           (mapcar (lambda (field)
445                                     `("  " ,(symbol-name field) " = {" ,field "},\n"))
446                                   fields)
447                           "}\n")))))))
448          doi-utils-bibtex-type-generators))
449
450 (doi-utils-def-bibtex-type article ("journal-article" "article-journal")
451                            author title journal year volume number pages doi url)
452
453 (doi-utils-def-bibtex-type inproceedings ("proceedings-article")
454                            author title booktitle year month pages doi url)
455
456 (doi-utils-def-bibtex-type book ("book")
457                            author title series publisher year pages doi url)
458
459 (doi-utils-def-bibtex-type inbook ("book-chapter")
460                            author title booktitle series publisher year pages doi url)
461
462
463
464 ;; With the code generating the bibtex entry in place, we can glue it to the json retrieval code.
465
466 (defun doi-utils-doi-to-bibtex-string (doi)
467   "return a bibtex entry as a string for the doi. Not all types are supported yet."
468   (let* ((results (doi-utils-get-json-metadata doi))
469          (type (plist-get results :type)))
470     ;(format "%s" results) ; json-data
471     (or (some (lambda (g) (funcall g type results)) doi-utils-bibtex-type-generators)
472         (message "%s not supported yet\n%S." type results))))
473
474 ;; That is just the string for the entry. To be useful, we need a function that inserts the string into a buffer. This function will insert the string at the cursor, clean the entry, try to get the pdf, and create a notes entry for you.
475
476
477 (defun doi-utils-insert-bibtex-entry-from-doi (doi)
478   "insert bibtex entry from a doi. Also cleans entry using
479 org-ref, and tries to download the corresponding pdf."
480   (interactive "sDOI :")
481   (insert (doi-utils-doi-to-bibtex-string doi))
482   (backward-char)
483   ;; set date added for the record
484   (bibtex-set-field "DATE_ADDED" (current-time-string))
485   (if (bibtex-key-in-head nil)
486        (org-ref-clean-bibtex-entry t)
487      (org-ref-clean-bibtex-entry))
488    ;; try to get pdf
489    (doi-utils-get-bibtex-entry-pdf)
490    (save-selected-window
491      (org-ref-open-bibtex-notes)))
492
493
494 ;; It may be you are in some other place when you want to add a bibtex entry. This next function will open the first entry in org-ref-default-bibliography go to the end, and add the entry. You can sort it later.
495
496
497 (defun doi-utils-add-bibtex-entry-from-doi (doi bibfile)
498   "Add entry to end of a file in in the current directory ending
499 with .bib or in `org-ref-default-bibliography'. If you have an
500 active region that starts like a DOI, that will be the initial
501 prompt. If no region is selected and the first entry of the
502 kill-ring starts like a DOI, then that is the intial
503 prompt. Otherwise, you have to type or pste in a DOI."
504   (interactive
505    (list (read-input "DOI: "
506                      ;; now set initial input
507                      (cond
508                       ;; If region is active and it starts like a doi we want it.
509                       ((and  (region-active-p)
510                              (s-match "^10" (buffer-substring
511                                               (region-beginning)
512                                               (region-end))))
513                        (buffer-substring (region-beginning) (region-end)))
514                       ;; if the first entry in the kill-ring looks
515                       ;; like a DOI, let's use it.
516                       ((if (s-match "^10" (car kill-ring))
517                            (car kill-ring)))
518                       ;; otherwise, we have no initial input. You
519                       ;; will have to type it in.
520                       (t
521                        nil)))
522          ;;  now get the bibfile to add it to
523          (ido-completing-read
524           "Bibfile: "
525           (append (f-entries "." (lambda (f) (f-ext? f "bib")))
526                   org-ref-default-bibliography))))
527   ;; Wrap in save-window-excursion to restore your window arrangement after this
528   ;; is done.
529   (save-window-excursion
530     (find-file bibfile)
531     ;; Check if the doi already exists
532     (goto-char (point-min))
533     (if (search-forward doi nil t)
534         (message "%s is already in this file" doi)
535       (end-of-buffer)
536       (insert "\n\n")
537       (doi-utils-insert-bibtex-entry-from-doi doi)
538       (save-buffer))))
539
540
541 ;; * Updating bibtex entries
542 ;; I wrote this code because it is pretty common for me to copy bibtex entries from ASAP articles that are incomplete, e.g. no page numbers because it is not in print yet. I wanted a convenient way to update an entry from its DOI. Basically, we get the metadata, and update the fields in the entry.
543
544 ;; There is not bibtex set field function, so I wrote this one.
545
546
547 (defun bibtex-set-field (field value &optional nodelim)
548   "set field to value in bibtex file. create field if it does not exist"
549   (interactive "sfield: \nsvalue: ")
550   (bibtex-beginning-of-entry)
551   (let ((found))
552     (if (setq found (bibtex-search-forward-field field t))
553         ;; we found a field
554         (progn
555           (goto-char (car (cdr found)))
556           (when value
557             (bibtex-kill-field)
558             (bibtex-make-field field nil nil nodelim)
559             (backward-char)
560             (insert value)))
561       ;; make a new field
562       (message "new field being made")
563       (bibtex-beginning-of-entry)
564       (forward-line) (beginning-of-line)
565       (bibtex-next-field nil)
566       (forward-char)
567       (bibtex-make-field field nil nil nodelim)
568       (backward-char)
569       (insert value))))
570
571
572 ;; The updating function for a whole entry looks like this. We get all the keys from the json plist metadata, and update the fields if they exist.
573
574
575 (defun plist-get-keys (plist)
576    "return keys in a plist"
577   (loop
578    for key in results by #'cddr collect key))
579
580 (defun doi-utils-update-bibtex-entry-from-doi (doi)
581   "update fields in a bibtex entry from the doi. Every field will be updated, so previous changes will be lost."
582   (interactive (list
583                 (or (replace-regexp-in-string "http://dx.doi.org/" "" (bibtex-autokey-get-field "doi"))
584                     (read-string "DOI: "))))
585   (let* ((results (doi-utils-get-json-metadata doi))
586          (type (plist-get results :type))
587          (author (mapconcat
588                   (lambda (x) (concat (plist-get x :given)
589                                     " " (plist-get x :family)))
590                   (plist-get results :author) " and "))
591          (title (plist-get results :title))
592          (journal (plist-get results :container-title))
593          (year (format "%s"
594                        (elt
595                         (elt
596                          (plist-get
597                           (plist-get results :issued) :date-parts) 0) 0)))
598         (volume (plist-get results :volume))
599         (number (or (plist-get results :issue) ""))
600         (pages (or (plist-get results :page) ""))
601         (url (or (plist-get results :URL) ""))
602         (doi (plist-get results :DOI)))
603
604     ;; map the json fields to bibtex fields. The code each field is mapped to is evaluated.
605     (setq mapping '((:author . (bibtex-set-field "author" author))
606                     (:title . (bibtex-set-field "title" title))
607                     (:container-title . (bibtex-set-field "journal" journal))
608                     (:issued . (bibtex-set-field "year" year))
609                     (:volume . (bibtex-set-field "volume" volume))
610                     (:issue . (bibtex-set-field "number" number))
611                     (:page . (bibtex-set-field "pages" pages))
612                     (:DOI . (bibtex-set-field "doi" doi))
613                     (:URL . (bibtex-set-field "url" url))))
614
615     ;; now we have code to run for each entry. we map over them and evaluate the code
616     (mapcar
617      (lambda (key)
618        (eval (cdr (assoc key mapping))))
619      (plist-get-keys results)))
620
621   ; reclean entry, but keep key if it exists.
622   (if (bibtex-key-in-head)
623       (org-ref-clean-bibtex-entry t)
624     (org-ref-clean-bibtex-entry)))
625
626
627 ;; A downside to updating an entry is it overwrites what you have already fixed. So, we next develop a function to update the field at point.
628
629
630 (defun doi-utils-update-field ()
631   (interactive)
632   (let* ((doi (bibtex-autokey-get-field "doi"))
633          (results (doi-utils-get-json-metadata doi))
634          (field (car (bibtex-find-text-internal nil nil ","))))
635     (cond
636      ((string= field "volume")
637       (bibtex-set-field field (plist-get results :volume)))
638      ((string= field "number")
639       (bibtex-set-field field (plist-get results :issue)))
640      ((string= field "pages")
641       (bibtex-set-field field (plist-get results :page)))
642      ((string= field "year")
643       (bibtex-set-field field (plist-get results :year)))
644      (t
645       (message "%s not supported yet." field)))))
646
647
648
649 ;; * DOI functions for WOS
650 ;; I came across this API http://wokinfo.com/media/pdf/OpenURL-guide.pdf to make links to the things I am interested in here. Based on that document, here are three links based on a doi:10.1021/jp047349j that take you to different Web Of Science (WOS) pages.
651
652
653 ;; 1. go to article in WOS: http://ws.isiknowledge.com/cps/openurl/service?url_ver=Z39.88-2004&rft_id=info:doi/10.1021/jp047349j
654 ;; 2. citing articles: http://ws.isiknowledge.com/cps/openurl/service?url_ver=Z39.88-2004&rft_id=info%3Adoi%2F10.1021/jp047349j&svc_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Asch_svc&svc.citing=yes
655 ;; 3. related articles: http://ws.isiknowledge.com/cps/openurl/service?url_ver=Z39.88-2004&rft_id=info%3Adoi%2F10.1021/jp047349j&svc_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Asch_svc&svc.related=yes
656
657 ;; These are pretty easy to construct, so we can write functions that will create them and open the url in our browser. There are some other options that could be considered, but since we usually have a doi, it seems like the best way to go for creating the links. Here are the functions.
658
659 (defun doi-utils-wos (doi)
660   "Open Web of Science entry for DOI"
661   (interactive "sDOI: ")
662   (browse-url
663    (format
664     "http://ws.isiknowledge.com/cps/openurl/service?url_ver=Z39.88-2004&rft_id=info:doi/%s" doi)))
665
666 (defun doi-utils-wos-citing (doi)
667   "Open Web of Science citing articles entry. May be empty if none are found"
668   (interactive "sDOI: ")
669   (browse-url
670    (concat
671     "http://ws.isiknowledge.com/cps/openurl/service?url_ver=Z39.88-2004&rft_id=info%3Adoi%2F"
672     doi
673     "&svc_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Asch_svc&svc.citing=yes")))
674
675 (defun doi-utils-wos-related (doi)
676   "Open Web of Science related articles page."
677   (interactive "sDOI: ")
678   (browse-url
679    (concat "http://ws.isiknowledge.com/cps/openurl/service?url_ver=Z39.88-2004&rft_id=info%3Adoi%2F"
680            doi
681            "&svc_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Asch_svc&svc.related=yes")))
682
683
684
685
686 ;; * A new doi link for org-mode
687 ;; The idea is to add a menu to the doi link, so rather than just clicking to open the article, you can do other things.
688 ;; 1. open doi
689 ;; 2. open in wos
690 ;; 3. open citing articles
691 ;; 4. open related articles
692 ;; 5. open bibtex entry
693 ;; 6. get bibtex entry
694
695
696 (defun doi-utils-open (doi)
697  (interactive "sDOI: ")
698  (browse-url (concat "http://dx.doi.org/" doi)))
699
700
701 (defun doi-utils-open-bibtex (doi)
702   "Search through `reftex-default-bibliography' for DOI."
703   (interactive "sDOI: ")
704   (catch 'file
705     (dolist (f reftex-default-bibliography)
706       (find-file f)
707       (when (search-forward doi (point-max) t)
708         (bibtex-beginning-of-entry)
709         (throw 'file t)))))
710
711
712 (defun doi-utils-crossref (doi)
713   "Search DOI in CrossRef."
714   (interactive "sDOI: ")
715   (browse-url
716    (format
717     "http://search.crossref.org/?q=%s" doi)))
718
719
720 (defun doi-utils-google-scholar (doi)
721   "Google scholar the word at point or selection."
722   (interactive "sDOI: ")
723   (browse-url
724    (format
725     "http://scholar.google.com/scholar?q=%s" doi)))
726
727
728 (defun doi-utils-pubmed (doi)
729   "Pubmed the word at point or selection."
730   (interactive "sDOI: ")
731   (browse-url
732    (format
733     "http://www.ncbi.nlm.nih.gov/pubmed/?term=%s"
734     (url-hexify-string doi))))
735
736
737 (defvar doi-link-menu-funcs '()
738  "Functions to run in doi menu. Each entry is a list of (key menu-name function).
739 The function must take one argument, the doi.")
740
741 (setq doi-link-menu-funcs
742       '(("o" "pen" doi-utils-open)
743         ("w" "os" doi-utils-wos)
744         ("c" "iting articles" doi-utils-wos-citing)
745         ("r" "elated articles" doi-utils-wos-related)
746         ("s" "Google Scholar" doi-utils-google-scholar)
747         ("f" "CrossRef" doi-utils-crossref)
748         ("p" "ubmed" doi-utils-pubmed)
749         ("b" "open in bibtex" doi-utils-open-bibtex)
750         ("g" "et bibtex entry" doi-utils-add-bibtex-entry-from-doi)))
751
752
753 (defun doi-link-menu (link-string)
754    "Generate the link menu message, get choice and execute it.
755 Options are stored in `doi-link-menu-funcs'."
756    (interactive)
757    (message
758    (concat
759     (mapconcat
760      (lambda (tup)
761        (concat "[" (elt tup 0) "]"
762                (elt tup 1) " "))
763      doi-link-menu-funcs "") ": "))
764    (let* ((input (read-char-exclusive))
765           (choice (assoc
766                    (char-to-string input) doi-link-menu-funcs)))
767      (when choice
768        (funcall
769         (elt
770          choice
771          2)
772         link-string))))
773
774 (org-add-link-type
775  "doi"
776  'doi-link-menu)
777
778
779 ;; * Getting a doi for a bibtex entry missing one
780 ;; Some bibtex entries do not have a DOI, maybe because they were entered by hand, or copied from a source that did not have it available. Here we develop some functions to help you find the DOI using Crossref.
781
782 ;; Here is our example bibtex entry.
783 ;; #+BEGIN_SRC bibtex
784 ;; @article{deml-2014-oxide,
785 ;;   author =    {Ann M. Deml and Vladan Stevanovi{\'c} and
786 ;;                   Christopher L. Muhich and Charles B. Musgrave and
787 ;;                   Ryan O'Hayre},
788 ;;   title =     {Oxide Enthalpy of Formation and Band Gap Energy As
789 ;;                   Accurate Descriptors of Oxygen Vacancy Formation
790 ;;                   Energetics},
791 ;;   journal =   {Energy Environ. Sci.},
792 ;;   volume =    7,
793 ;;   number =    6,
794 ;;   pages =     1996,
795 ;;   year =      2014,
796 ;;   doi =               {10.1039/c3ee43874k,
797 ;;   url =               {http://dx.doi.org/10.1039/c3ee43874k}},
798
799 ;; }
800
801
802 ;; The idea is to query Crossref in a way that is likely to give us a hit relevant to the entry.
803
804 ;; According to http://search.crossref.org/help/api we can send a query with a free form citation that may give us something back. We do this to get a list of candidates, and run a helm command to get the doi.
805
806
807
808 (defun doi-utils-crossref-citation-query ()
809   "Query Crossref with the title of the bibtex entry at point to
810 get a list of possible matches. This opens a helm buffer to
811 select an entry. The default action inserts a doi and url field
812 in the bibtex entry at point. The second action opens the doi
813 url. If there is already a doi field, the function raises an
814 error."
815   (interactive)
816   (bibtex-beginning-of-entry)
817   (let* ((entry (bibtex-parse-entry))
818          (json-string)
819          (json-data)
820          (doi))
821     (unless (string= ""(reftex-get-bib-field "doi" entry))
822       (error "Entry already has a doi field"))
823
824     (with-current-buffer
825         (url-retrieve-synchronously
826          (concat
827           "http://search.crossref.org/dois?q="
828           (url-hexify-string (org-ref-bib-citation))))
829       (setq json-string (buffer-substring url-http-end-of-headers (point-max)))
830       (setq json-data (json-read-from-string json-string)))
831
832     (let* ((name (format "Crossref hits for %s" (org-ref-bib-citation)))
833            (helm-candidates (mapcar (lambda (x)
834                                       (cons
835                                        (concat
836                                         (cdr (assoc 'fullCitation x))
837                                         " "
838                                         (cdr (assoc 'doi x)))
839                                        (cdr (assoc 'doi x))))
840                                       json-data))
841            (source `((name . ,name)
842                      (candidates . ,helm-candidates)
843                      ;; just return the candidate
844                      (action . (("Insert doi and url field" . (lambda (doi)
845                                                                 (bibtex-make-field "doi")
846                                                                 (backward-char)
847                                                                 ;; crossref returns doi url, but I prefer only a doi for the doi field
848                                                                 (insert (replace-regexp-in-string "^http://dx.doi.org/" "" doi))
849                                                                 (when (string= ""(reftex-get-bib-field "url" entry))
850                                                                   (bibtex-make-field "url")
851                                                                   (backward-char)
852                                                                   (insert doi))))
853                                 ("Open url" . (lambda (doi)
854                                                 (browse-url doi))))))))
855       (helm :sources '(source)))))
856
857
858
859 ;; * Debugging a DOI
860 ;; I wrote this function to help debug a DOI. This function generates an org-buffer with the doi, gets the json metadata, shows the bibtex entry, and the pdf link for it.
861
862 (defun doi-utils-debug (doi)
863   "Generate an org-buffer showing data about DOI."
864   (interactive "sDOI: ")
865   (switch-to-buffer "*debug-doi*")
866   (erase-buffer)
867   (org-mode)
868   (insert (concat "doi:" doi) "\n\n")
869   (insert "* JSON
870 " (format "%s" (doi-utils-get-json-metadata doi)) "
871
872 * Bibtex
873
874 " (doi-utils-doi-to-bibtex-string doi) "
875
876 * PDF
877 " (doi-utils-get-pdf-url doi)))
878
879 ;; * Adding a bibtex entry from a crossref query
880 ;; The idea here is to perform a query on Crossref, get a helm buffer of candidates, and select the entry(ies) you want to add to your bibtex file. You can select a region, e.g. a free form citation, or set of words, or you can type the query in by hand.
881
882 (defun doi-utils-add-entry-from-crossref-query (query bibtex-file)
883   (interactive (list
884                 (read-input
885                  "Query: "
886                  ;; now set initial input
887                  (cond
888                   ;; If region is active assume we want it
889                   ((region-active-p)
890                    (replace-regexp-in-string
891                     "\n" " "
892                     (buffer-substring (region-beginning) (region-end))))
893                   ;; type or paste it in
894                   (t
895                    nil)))
896                 (ido-completing-read
897                  "Bibfile: "
898                  (append (f-entries "." (lambda (f) (f-ext? f "bib")))
899                          org-ref-default-bibliography))))
900   (let* ((json-string)
901          (json-data)
902          (doi))
903
904     (with-current-buffer
905         (url-retrieve-synchronously
906          (concat
907           "http://search.crossref.org/dois?q="
908           (url-hexify-string query)))
909       (setq json-string (buffer-substring url-http-end-of-headers (point-max)))
910       (setq json-data (json-read-from-string json-string)))
911
912     (let* ((name (format "Crossref hits for %s"
913                          ;; remove carriage returns. they cause problems in helm.
914                          (replace-regexp-in-string "\n" " " query)))
915            (helm-candidates (mapcar (lambda (x)
916                                       (cons
917                                        (concat
918                                         (cdr (assoc 'fullCitation x))
919                                         " "
920                                         (cdr (assoc 'doi x)))
921                                        (cdr (assoc 'doi x))))
922                                       json-data))
923            (source `((name . ,name)
924                      (candidates . ,helm-candidates)
925                      ;; just return the candidate
926                      (action . (("Insert bibtex entry" . (lambda (doi)
927                                                            (doi-utils-add-bibtex-entry-from-doi
928                                                             (replace-regexp-in-string "^http://dx.doi.org/" "" doi) ,bibtex-file)))
929                                 ("Open url" . (lambda (doi)
930                                                 (browse-url doi))))))))
931       (helm :sources '(source)))))
932
933 ;; * The end
934 (provide 'doi-utils)
935 ;;; doi-utils.el ends here