d1d3efb9084ec199bb0736f844db217a481ef36d
[debian/jabref.git] / src / java / net / sf / jabref / imports / ACMPortalFetcher.java
1 /*  Copyright (C) 2003-2011 Aaron Chen
2     This program is free software; you can redistribute it and/or modify
3     it under the terms of the GNU General Public License as published by
4     the Free Software Foundation; either version 2 of the License, or
5     (at your option) any later version.
6
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10     GNU General Public License for more details.
11
12     You should have received a copy of the GNU General Public License along
13     with this program; if not, write to the Free Software Foundation, Inc.,
14     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
15 */
16 package net.sf.jabref.imports;
17
18 import java.awt.*;
19 import java.io.*;
20 import java.net.ConnectException;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.util.*;
24 import java.util.regex.Matcher;
25 import java.util.regex.Pattern;
26
27 import javax.swing.*;
28
29 import net.sf.jabref.BibtexEntry;
30 import net.sf.jabref.GUIGlobals;
31 import net.sf.jabref.Globals;
32 import net.sf.jabref.OutputPrinter;
33 import net.sf.jabref.gui.FetcherPreviewDialog;
34
35 public class ACMPortalFetcher implements PreviewEntryFetcher {
36
37         private ImportInspector dialog = null;
38         private OutputPrinter status;
39     private final HTMLConverter htmlConverter = new HTMLConverter();
40     private String terms;
41     
42     private static final String startUrl = "http://portal.acm.org/";
43     private static final String searchUrlPart = "results.cfm?query=";
44     private static final String searchUrlPartII = "&dl=";
45     private static final String endUrl = "&coll=Portal&short=0";//&start=";
46
47     private static final String bibtexUrl = "exportformats.cfm?id=";
48     private static final String bibtexUrlEnd = "&expformat=bibtex";
49     private static final String abstractUrl = "tab_abstract.cfm?id=";
50     
51     private final JRadioButton acmButton = new JRadioButton(Globals.lang("The ACM Digital Library"));
52     private final JRadioButton guideButton = new JRadioButton(Globals.lang("The Guide to Computing Literature"));
53     private final JCheckBox absCheckBox = new JCheckBox(Globals.lang("Include abstracts"), false);
54     
55     private static final int perPage = 20;
56     private static final int MAX_FETCH = perPage; // only one page. Otherwise, the user will get blocked by ACM. 100 has been the old setting. See Bug 3532752 - https://sourceforge.net/tracker/index.php?func=detail&aid=3532752&group_id=92314&atid=600306
57     private static final int WAIT_TIME = 200;
58     private int hits = 0, unparseable = 0, parsed = 0;
59     private boolean shouldContinue = false;
60     
61     // user settings
62     private boolean fetchAbstract = false;
63     private boolean acmOrGuide = false;
64
65     private static final Pattern hitsPattern = Pattern.compile(".*Found <b>(\\d+,*\\d*)</b>.*");
66     private static final Pattern maxHitsPattern = Pattern.compile(".*Results \\d+ - \\d+ of (\\d+,*\\d*).*");
67     //private static final Pattern bibPattern = Pattern.compile(".*'(exportformats.cfm\\?id=\\d+&expformat=bibtex)'.*");
68     
69     private static final Pattern fullCitationPattern =
70         Pattern.compile("<A HREF=\"(citation.cfm.*)\" class.*");
71
72     private static final Pattern idPattern =
73         Pattern.compile("citation.cfm\\?id=\\d*\\.?(\\d+)&.*");
74
75     // Patterns used to extract information for the preview:
76     private static final Pattern titlePattern = Pattern.compile("<A HREF=.*?\">([^<]*)</A>");
77     private static final Pattern monthYearPattern = Pattern.compile("([A-Za-z]+ [0-9]{4})");
78     private static final Pattern absPattern = Pattern.compile("<div .*?>(.*?)</div>");
79     private FetcherPreviewDialog preview;
80
81     public JPanel getOptionsPanel() {
82         JPanel pan = new JPanel();
83         pan.setLayout(new GridLayout(0,1));
84
85         guideButton.setSelected(true);
86         
87         ButtonGroup group = new ButtonGroup();
88         group.add(acmButton);
89         group.add(guideButton);
90         
91         pan.add(absCheckBox);
92         pan.add(acmButton);
93         pan.add(guideButton);
94         
95         return pan;
96     }
97
98     public boolean processQueryGetPreview(String query, FetcherPreviewDialog preview, OutputPrinter status) {
99         this.preview = preview;
100         this.status = status;
101         this.terms = query;
102         piv = 0;
103         shouldContinue = true;
104         parsed = 0;
105         unparseable = 0;
106         acmOrGuide = acmButton.isSelected();
107         fetchAbstract = absCheckBox.isSelected();
108         int firstEntry = 1;
109         String address = makeUrl(firstEntry);
110         LinkedHashMap<String, JLabel> previews = new LinkedHashMap<String, JLabel>();
111
112         try {
113             URL url = new URL(address);
114
115             String page = getResults(url);
116
117             hits = getNumberOfHits(page, "Found", hitsPattern);
118
119                         int index = page.indexOf("Found");
120                         if (index >= 0) {
121                 page = page.substring(index + 5);
122                                 index = page.indexOf("Found");
123                                 if (index >= 0)
124                         page = page.substring(index);
125                         }
126
127             if (hits == 0) {
128                 status.showMessage(Globals.lang("No entries found for the search string '%0'",
129                         terms),
130                         Globals.lang("Search ACM Portal"), JOptionPane.INFORMATION_MESSAGE);
131                 return false;
132             }
133
134             hits = getNumberOfHits(page, "Results", maxHitsPattern);
135
136             for (int i=0; i<hits; i++) {
137                 parse(page, 0, firstEntry, previews);
138                 //address = makeUrl(firstEntry);
139                 firstEntry += perPage;
140             }
141             for (String s : previews.keySet()) {
142                 preview.addEntry(s, previews.get(s));
143             }
144
145
146             return true;
147
148         } catch (MalformedURLException e) {
149             e.printStackTrace();
150         } catch (ConnectException e) {
151             status.showMessage(Globals.lang("Connection to ACM Portal failed"),
152                     Globals.lang("Search ACM Portal"), JOptionPane.ERROR_MESSAGE);
153         } catch (IOException e) {
154                 status.showMessage(Globals.lang(e.getMessage()),
155                     Globals.lang("Search ACM Portal"), JOptionPane.ERROR_MESSAGE);
156             e.printStackTrace();
157         }
158         return false;
159
160     }
161
162     public void getEntries(Map<String, Boolean> selection, ImportInspector inspector) {
163         for (String id : selection.keySet()) {
164             if (!shouldContinue)
165                 break;
166             boolean sel = selection.get(id);
167             if (sel) {
168                 try {
169                     BibtexEntry entry = downloadEntryBibTeX(id, fetchAbstract);
170                     inspector.addEntry(entry);
171                 } catch (IOException e) {
172                     e.printStackTrace();
173                 }
174             }
175         }
176     }
177
178     public int getWarningLimit() {
179         return 10;
180     }
181
182     public int getPreferredPreviewHeight() {
183         return 75;
184     }
185
186     public boolean processQuery(String query, ImportInspector dialog, OutputPrinter status) {
187         return false;
188     }
189
190     private String makeUrl(int startIndex) {
191         StringBuffer sb = new StringBuffer(startUrl).append(searchUrlPart);
192         sb.append(terms.replaceAll(" ", "%20"));
193         sb.append("&start=" + String.valueOf(startIndex));
194         sb.append(searchUrlPartII);
195   
196         if (acmOrGuide)
197                 sb.append("ACM");
198         else
199                 sb.append("GUIDE");
200         sb.append(endUrl);
201         return sb.toString();
202     }
203
204     private int piv = 0;
205
206     private void parse(String text, int startIndex, int firstEntryNumber, Map<String,JLabel> entries) {
207         piv = startIndex;
208         int entryNumber = firstEntryNumber;
209         String entry;
210         while (getNextEntryURL(text, piv, entryNumber, entries)) {
211             entryNumber++;
212         }
213
214     }
215
216     private String getEntryBibTeXURL(String fullCitation, boolean abs) {
217         String bibAddr = "";
218         String ID = "";
219
220         // Get ID
221         Matcher idMatcher = idPattern.matcher(fullCitation);
222         if (idMatcher.find()) {
223             ID = idMatcher.group(1);
224             //System.out.println("To fetch: " + bibAddr);
225         }
226         else {
227             System.out.println("Did not find ID in: " + fullCitation);
228             return null;
229         }
230
231         // fetch bibtex record
232         //bibAddr = bibtexUrl + ID + bibtexUrlEnd;
233         return ID;
234
235     }
236
237     private boolean getNextEntryURL(String allText, int startIndex, int entryNumber,
238                                    Map<String,JLabel> entries) {
239         String toFind = new StringBuffer().append("<strong>")
240                 .append(entryNumber).append("</strong><br>").toString();
241         int index = allText.indexOf(toFind, startIndex);
242         int endIndex = allText.length();
243
244         if (index >= 0) {
245             piv = index+1;
246             String text = allText.substring(index, endIndex);
247             // Always try RIS import first
248                         Matcher fullCitation =
249                                 fullCitationPattern.matcher(text);
250                         if (fullCitation.find()) {
251                 String link = getEntryBibTeXURL(fullCitation.group(1), fetchAbstract);
252                 String part;
253                 int endOfRecord = text.indexOf("<div class=\"abstract2\">");
254                 if (endOfRecord > 0) {
255                     StringBuilder sb = new StringBuilder();
256                     part = text.substring(0, endOfRecord);
257
258                     try {
259                         save("part"+entryNumber+".html", part);
260                     } catch (IOException e) {
261                         e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
262                     }
263
264                     // Find authors:
265                     String authMarker = "<div class=\"authors\">";
266                     int authStart = text.indexOf(authMarker);
267                     if (authStart >= 0) {
268                         int authEnd = text.indexOf("</div>", authStart+authMarker.length());
269                         if (authEnd >= 0) {
270                             sb.append("<p>"+text.substring(authStart, authEnd)+"</p>");
271                         }
272
273                     }
274                     // Find title:
275                     Matcher titM = titlePattern.matcher(part);
276                     if (titM.find())
277                         sb.append("<p>"+titM.group(1)+"</p>");
278                     // Find month and year:
279                     Matcher mY = monthYearPattern.matcher(part);
280                     if (mY.find())
281                         sb.append("<p>"+mY.group(1)+"</p>");
282
283
284                     part = sb.toString();
285                     /*.replaceAll("</tr>", "<br>");
286                     part = part.replaceAll("</td>", "");
287                     part = part.replaceAll("<tr valign=\"[A-Za-z]*\">", "");
288                     part = part.replaceAll("<table style=\"padding: 5px; 5px; 5px; 5px;\" border=\"0\">", "");*/
289                 }
290                 else part = link;
291
292
293                 JLabel preview = new JLabel("<html>"+part+"</html>");
294                 preview.setPreferredSize(new Dimension(750, 100));
295                 entries.put(link, preview);
296                 return true;
297                         } else {
298                                 System.out.printf("Citation Unmatched %d\n", entryNumber);
299                                 System.out.printf(text);
300                 return false;
301                         }
302
303         }
304
305         return false;
306     }
307
308     private BibtexEntry downloadEntryBibTeX(String ID, boolean abs) throws IOException {
309                 try {
310                     URL url = new URL(startUrl+bibtexUrl+ID+bibtexUrlEnd);
311                         BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
312                         ParserResult result = BibtexParser.parse(in);
313                         in.close();
314                         Collection<BibtexEntry> item = result.getDatabase().getEntries();
315                 if (item.size() == 0)
316                     return null;
317                         BibtexEntry entry = item.iterator().next();
318                         Thread.sleep(WAIT_TIME);//wait between requests or you will be blocked by ACM
319
320                 // get abstract
321                 if (abs) {
322                         url = new URL(startUrl + abstractUrl + ID);
323                         String page = getResults(url);
324                     Matcher absM = absPattern.matcher(page);
325                     if (absM.find()) {
326                         entry.setField("abstract", absM.group(1).trim());
327                     }
328                                 Thread.sleep(WAIT_TIME);//wait between requests or you will be blocked by ACM
329                 }
330
331                         return entry;
332
333             } catch (NoSuchElementException e) {
334                 System.out.println("Bad Bibtex record read at: " + bibtexUrl + ID + bibtexUrlEnd);
335                 e.printStackTrace();
336                 return null;
337             } catch (MalformedURLException e) {
338                 e.printStackTrace();
339                 return null;
340             } catch (ConnectException e) {
341                 e.printStackTrace();
342                 return null;
343                 } catch (IOException e) {
344                         e.printStackTrace();
345                         return null;
346                 } catch (InterruptedException e) {
347                         e.printStackTrace();
348                         return null;
349                 }
350         }
351
352
353     /**
354      * This method must convert HTML style char sequences to normal characters.
355      * @param text The text to handle.
356      * @return The converted text.
357      */
358     private String convertHTMLChars(String text) {
359
360         return htmlConverter.format(text);
361     }
362
363
364     /**
365      * Find out how many hits were found.
366      * @param page
367      */
368     private int getNumberOfHits(String page, String marker, Pattern pattern) throws IOException {
369         int ind = page.indexOf(marker);
370         if (ind < 0) {
371                 throw new IOException(Globals.lang("Could not parse number of hits"));
372         }
373         String substring = page.substring(ind, Math.min(ind + 42, page.length()));
374         Matcher m = pattern.matcher(substring);
375         if (!m.find()) {
376                 System.out.println("Unmatched!");
377                 System.out.println(substring);
378         } else {
379             try {
380                 // get rid of ,
381                 String number = m.group(1);
382                 //NumberFormat nf = NumberFormat.getInstance();
383                 //return nf.parse(number).intValue();
384                 number = number.replaceAll(",", "");
385                 //System.out.println(number);
386                 return Integer.parseInt(number);
387             } catch (NumberFormatException ex) {
388                 throw new IOException(Globals.lang("Could not parse number of hits"));
389             } catch (IllegalStateException e) {
390                 throw new IOException(Globals.lang("Could not parse number of hits"));
391             }
392         }
393         throw new IOException(Globals.lang("Could not parse number of hits"));
394     }
395
396     /**
397      * Download the URL and return contents as a String.
398      * @param source
399      * @return
400      * @throws IOException
401      */
402     public String getResults(URL source) throws IOException {
403         
404         InputStream in = source.openStream();
405         StringBuffer sb = new StringBuffer();
406         byte[] buffer = new byte[256];
407         while(true) {
408             int bytesRead = in.read(buffer);
409             if(bytesRead == -1) break;
410             for (int i=0; i<bytesRead; i++)
411                 sb.append((char)buffer[i]);
412         }
413         return sb.toString();
414     }
415
416     /**
417      * Read results from a file instead of an URL. Just for faster debugging.
418      * @param f
419      * @return
420      * @throws IOException
421      */
422     public String getResultsFromFile(File f) throws IOException {
423         InputStream in = new BufferedInputStream(new FileInputStream(f));
424         StringBuffer sb = new StringBuffer();
425         byte[] buffer = new byte[256];
426         while(true) {
427             int bytesRead = in.read(buffer);
428             if(bytesRead == -1) break;
429             for (int i=0; i<bytesRead; i++)
430                 sb.append((char)buffer[i]);
431         }
432         return sb.toString();
433     }
434
435         public String getTitle() {
436             return "ACM Portal";
437         }
438         
439         
440         public URL getIcon() {
441             return GUIGlobals.getIconUrl("www");
442         }
443         
444         public String getHelpPage() {
445             return "ACMPortalHelp.html";
446         }
447         
448         public String getKeyName() {
449             return "ACM Portal";
450         }
451         
452         // This method is called by the dialog when the user has cancelled the import.
453         public void cancelled() {
454             shouldContinue = false;
455         }
456         
457         // This method is called by the dialog when the user has selected the
458         //wanted entries, and clicked Ok. The callback object can update status
459         //line etc.
460         public void done(int entriesImported) {
461
462         }
463         
464         // This method is called by the dialog when the user has cancelled or
465         //signalled a stop. It is expected that any long-running fetch operations
466         //will stop after this method is called.
467         public void stopFetching() {
468             shouldContinue = false;
469         }
470
471
472     private void save(String filename, String content) throws IOException {
473         BufferedWriter out = new BufferedWriter(new FileWriter(filename));
474         out.write(content);
475         out.close();
476     }
477 }