ad37e2e4275ea1234718f350f428e4ce4b754a1e
[debian/jabref.git] / src / java / net / sf / jabref / imports / BibtexParser.java
1 /*
2 Copyright (C) 2003 David Weitzman, Nizar N. Batada, Morten O. Alver
3
4 All programs in this directory and
5 subdirectories are published under the GNU General Public License as
6 described below.
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or (at
11 your option) any later version.
12
13 This program is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
21 USA
22
23 Further information about the GNU GPL is available at:
24 http://www.gnu.org/copyleft/gpl.ja.html
25
26 */
27
28 package net.sf.jabref.imports;
29
30 import java.io.*;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.StringTokenizer;
34 import java.util.regex.Pattern;
35
36 import net.sf.jabref.*;
37
38 public class BibtexParser
39 {
40     private PushbackReader _in;
41     private BibtexDatabase _db;
42     private HashMap _meta, entryTypes;
43     private boolean _eof = false;
44     private int line = 1;
45     private FieldContentParser fieldContentParser = new FieldContentParser();
46
47     public BibtexParser(Reader in)
48     {
49         if (in == null)
50         {
51             throw new NullPointerException();
52         }
53
54         _in = new PushbackReader(in);
55
56     }
57
58     /**
59    * Check whether the source is in the correct format for this importer.
60    */
61   public static boolean isRecognizedFormat(Reader inOrig)
62     throws IOException {
63     // Our strategy is to look for the "PY <year>" line.
64     BufferedReader in =
65       new BufferedReader(inOrig);
66
67     //Pattern pat1 = Pattern.compile("PY:  \\d{4}");
68     Pattern pat1 = Pattern.compile("@[a-zA-Z]*\\s*\\{");
69
70     String str;
71
72     while ((str = in.readLine()) != null) {
73
74       if (pat1.matcher(str).find())
75         return true;
76       else if (str.startsWith(GUIGlobals.SIGNATURE))
77         return true;
78     }
79
80     return false;
81   }
82
83     private void skipWhitespace() throws IOException
84     {
85         int c;
86
87         while (true)
88         {
89             c = read();
90             if ((c == -1) || (c == 65535))
91             {
92                 _eof = true;
93                 return;
94             }
95
96             if (Character.isWhitespace((char) c))
97             {
98                 continue;
99             }
100             else
101             // found non-whitespace char
102             //Util.pr("SkipWhitespace, stops: "+c);
103             unread(c);
104             /*      try {
105                 Thread.currentThread().sleep(500);
106                 } catch (InterruptedException ex) {}*/
107             break;
108         }
109     }
110
111     private String skipAndRecordWhitespace(int j) throws IOException
112     {
113         int c;
114         StringBuffer sb = new StringBuffer();
115         if (j != ' ')
116             sb.append((char)j);
117         while (true)
118         {
119             c = read();
120             if ((c == -1) || (c == 65535))
121             {
122                 _eof = true;
123                 return sb.toString();
124             }
125
126             if (Character.isWhitespace((char) c))
127             {
128                 if (c != ' ')
129                     sb.append((char)c);
130                 continue;
131             }
132             else
133             // found non-whitespace char
134             //Util.pr("SkipWhitespace, stops: "+c);
135         unread(c);
136             /*      try {
137                 Thread.currentThread().sleep(500);
138                 } catch (InterruptedException ex) {}*/
139             break;
140         }
141         return sb.toString();
142     }
143
144
145     public ParserResult parse() throws IOException {
146
147         _db = new BibtexDatabase(); // Bibtex related contents.
148         _meta = new HashMap();      // Metadata in comments for Bibkeeper.
149         entryTypes = new HashMap(); // To store custem entry types parsed.
150         ParserResult _pr = new ParserResult(_db, _meta, entryTypes);
151         skipWhitespace();
152
153         try
154         {
155             while (!_eof)
156             {
157                 boolean found = consumeUncritically('@');
158                 if (!found)
159                     break;
160                 skipWhitespace();
161                 String entryType = parseTextToken();
162                 BibtexEntryType tp = BibtexEntryType.getType(entryType);
163                 boolean isEntry = (tp != null);
164             //Util.pr(tp.getName());
165                 if (!isEntry) {
166                     // The entry type name was not recognized. This can mean
167                     // that it is a string, preamble, or comment. If so,
168                     // parse and set accordingly. If not, assume it is an entry
169                     // with an unknown type.
170                     if (entryType.toLowerCase().equals("preamble")) {
171                         _db.setPreamble(parsePreamble());
172                     }
173                     else if (entryType.toLowerCase().equals("string")) {
174                         BibtexString bs = parseString();
175                         try {
176                             _db.addString(bs);
177                         } catch (KeyCollisionException ex) {
178                             _pr.addWarning(Globals.lang("Duplicate string name")+": "+bs.getName());
179                             //ex.printStackTrace();
180                         }
181                     }
182                     else if (entryType.toLowerCase().equals("comment")) {
183                         StringBuffer commentBuf = parseBracketedTextExactly();
184                         /**
185                          *
186                          * Metadata are used to store Bibkeeper-specific
187                          * information in .bib files.
188                          *
189                          * Metadata are stored in bibtex files in the format
190                          * @comment{jabref-meta: type:data0;data1;data2;...}
191                          *
192                          * Each comment that starts with the META_FLAG is stored
193                          * in the meta HashMap, with type as key.
194                          * Unluckily, the old META_FLAG bibkeeper-meta: was used
195                          * in JabRef 1.0 and 1.1, so we need to support it as
196                          * well. At least for a while. We'll always save with the
197                          * new one.
198                          */
199                         String comment = commentBuf.toString().replaceAll("[\\x0d\\x0a]","");
200             if (comment.substring(0, Math.min(comment.length(), GUIGlobals.META_FLAG.length()))
201                             .equals(GUIGlobals.META_FLAG) ||
202                             comment.substring(0, Math.min(comment.length(), GUIGlobals.META_FLAG_OLD.length()))
203                             .equals(GUIGlobals.META_FLAG_OLD)) {
204
205                             String rest;
206                             if (comment.substring(0, GUIGlobals.META_FLAG.length())
207                                 .equals(GUIGlobals.META_FLAG))
208                                 rest = comment.substring
209                                     (GUIGlobals.META_FLAG.length());
210                             else
211                                 rest = comment.substring
212                                     (GUIGlobals.META_FLAG_OLD.length());
213
214                             int pos = rest.indexOf(':');
215
216                             if (pos > 0)
217                                 _meta.put
218                                     (rest.substring(0, pos), rest.substring(pos+1));
219                     // We remove all line breaks in the metadata - these will have been inserted
220                     // to prevent too long lines when the file was saved, and are not part of the data.
221                         }
222
223                         /**
224                          * A custom entry type can also be stored in a @comment:
225                          */
226                         if (comment.substring(0, Math.min(comment.length(), GUIGlobals.ENTRYTYPE_FLAG.length()))
227                             .equals(GUIGlobals.ENTRYTYPE_FLAG)) {
228
229                             CustomEntryType typ = CustomEntryType.parseEntryType(comment);
230                             entryTypes.put(typ.getName().toLowerCase(), typ);
231
232                         }
233                     }
234                     else {
235                         // The entry type was not recognized. This may mean that
236                         // it is a custom entry type whose definition will appear
237                         // at the bottom of the file. So we use an UnknownEntryType
238                         // to remember the type name by.
239                         tp = new UnknownEntryType(entryType.toLowerCase());
240                         //System.out.println("unknown type: "+entryType);
241                         isEntry = true;
242                     }
243                 }
244
245                 if (isEntry) // True if not comment, preamble or string.
246                 {
247                     BibtexEntry be = parseEntry(tp);
248
249                     boolean duplicateKey = _db.insertEntry(be);
250                     if (duplicateKey) // JZTODO lyrics
251                       _pr.addWarning(Globals.lang("duplicate BibTeX key")+": "+be.getCiteKey()
252                               + " (" + "Grouping may not work for this entry." + ")");
253                     else if (be.getCiteKey() == null || be.getCiteKey().equals("")) {
254                         _pr.addWarning(Globals.lang("empty BibTeX key")+": "+be.getAuthorTitleYear(40)
255                                 + " (" + "Grouping may not work for this entry." + ")");
256                     }
257                 }
258
259                 skipWhitespace();
260         }
261
262             // Before returning the database, update entries with unknown type
263             // based on parsed type definitions, if possible.
264             checkEntryTypes(_pr);
265
266             return _pr;
267         }
268         catch (KeyCollisionException kce)
269         {
270           //kce.printStackTrace();
271             throw new IOException("Duplicate ID in bibtex file: " +
272                 kce.toString());
273         }
274     }
275
276     private int peek() throws IOException
277     {
278         int c = read();
279         unread(c);
280
281         return c;
282     }
283
284     private int read() throws IOException
285     {
286         int c = _in.read();
287         if (c == '\n')
288             line++;
289     return c;
290     }
291
292     private void unread(int c) throws IOException
293     {
294         if (c == '\n')
295             line--;
296         _in.unread(c);
297     }
298
299     public BibtexString parseString() throws IOException
300     {
301         //Util.pr("Parsing string");
302         skipWhitespace();
303         consume('{','(');
304         //while (read() != '}');
305         skipWhitespace();
306         //Util.pr("Parsing string name");
307         String name = parseTextToken();
308         //Util.pr("Parsed string name");
309         skipWhitespace();
310         //Util.pr("Now the contents");
311         consume('=');
312         String content = parseFieldContent();
313         //Util.pr("Now I'm going to consume a }");
314         consume('}',')');
315         //Util.pr("Finished string parsing.");
316         String id = Util.createNeutralId();
317         return new BibtexString(id, name, content);
318     }
319
320     public String parsePreamble() throws IOException
321     {
322         return parseBracketedText().toString();
323     }
324
325     public BibtexEntry parseEntry(BibtexEntryType tp) throws IOException
326     {
327     String id = Util.createNeutralId();//createId(tp, _db);
328     BibtexEntry result = new BibtexEntry(id, tp);
329     skipWhitespace();
330         consume('{','(');
331         skipWhitespace();
332         String key = null;
333         boolean doAgain = true;
334         while (doAgain) {
335             doAgain = false;
336             try {
337                 if (key != null)
338                     key = key+parseKey();//parseTextToken(),
339                 else key = parseKey();
340             } catch (NoLabelException ex) {
341                 // This exception will be thrown if the entry lacks a key
342                 // altogether, like in "@article{ author = { ...".
343                 // It will also be thrown if a key contains =.
344                 char c = (char)peek();
345                 if (Character.isWhitespace(c) || (c == '{')
346                     || (c == '\"')) {
347                     String fieldName = ex.getMessage().trim().toLowerCase();
348                     String cont = parseFieldContent();
349             result.setField(fieldName, cont);
350                 } else {
351                     if (key != null)
352                         key = key+ex.getMessage()+"=";
353                     else key = ex.getMessage()+"=";
354                     doAgain = true;
355                 }
356             }
357         }
358
359         if ((key != null) && key.equals(""))
360             key = null;
361         //System.out.println("Key: "+key);
362         if(result!=null)result.setField(BibtexFields.KEY_FIELD, key);
363     skipWhitespace();
364
365         while (true)
366         {
367             int c = peek();
368             if ((c == '}') || (c == ')'))
369             {
370                 break;
371             }
372
373             if (c == ',')
374                 consume(',');
375
376             skipWhitespace();
377
378             c = peek();
379             if ((c == '}') || (c == ')'))
380             {
381                 break;
382             }
383             parseField(result);
384         }
385
386         consume('}',')');
387     return result;
388     }
389
390     private void parseField(BibtexEntry entry) throws IOException
391     {
392         String key = parseTextToken().toLowerCase();
393             //Util.pr("Field: _"+key+"_");
394         skipWhitespace();
395         consume('=');
396         String content = parseFieldContent();
397         // Now, if the field in question is set up to be fitted automatically with braces around
398         // capitals, we should remove those now when reading the field:
399         if (Globals.prefs.putBracesAroundCapitals(key)) {
400             content = Util.removeBracesAroundCapitals(content);
401         }
402     if (content.length() > 0) {
403           if (entry.getField(key) == null)
404             entry.setField(key, content);
405           else {
406             // The following hack enables the parser to deal with multiple author or
407             // editor lines, stringing them together instead of getting just one of them.
408             // Multiple author or editor lines are not allowed by the bibtex format, but
409             // at least one online database exports bibtex like that, making it inconvenient
410             // for users if JabRef didn't accept it.
411             if (key.equals("author") || key.equals("editor"))
412               entry.setField(key, entry.getField(key)+" and "+content);
413           }
414         }
415     }
416
417     private String parseFieldContent() throws IOException
418     {
419         skipWhitespace();
420         StringBuffer value = new StringBuffer();
421         int c,j='.';
422
423         while (((c = peek()) != ',') && (c != '}') && (c != ')'))
424         {
425
426             if (_eof) {
427                         throw new RuntimeException("Error in line "+line+
428                                                    ": EOF in mid-string");
429             }
430             if (c == '"')
431             {
432                 // value is a string
433                 consume('"');
434
435                 while (!((peek() == '"') && (j != '\\')))
436                 {
437                     j = read();
438                     if (_eof || (j == -1) || (j == 65535))
439                     {
440                         throw new RuntimeException("Error in line "+line+
441                                                    ": EOF in mid-string");
442                     }
443
444                     value.append((char) j);
445                 }
446
447                 consume('"');
448
449             }
450             else if (c == '{') {
451                 // Value is a string enclosed in brackets. There can be pairs
452                 // of brackets inside of a field, so we need to count the brackets
453                 // to know when the string is finished.
454
455                 //if (isStandardBibtexField || !Globals.prefs.getBoolean("preserveFieldFormatting")) {
456                     // value.append(parseBracketedText());
457                     // TEST TEST TEST TEST TEST
458                 StringBuffer text = parseBracketedTextExactly();
459                 value.append(fieldContentParser.format(text));
460                 //}
461                 //else
462                 //    value.append(parseBracketedTextExactly());
463             }
464             else if (Character.isDigit((char) c))
465             {
466                 // value is a number
467                 String numString = parseTextToken();
468                 int numVal = Integer.parseInt(numString);
469                 value.append((new Integer(numVal)).toString());
470                 //entry.setField(key, new Integer(numVal));
471             }
472             else if (c == '#')
473             {
474                 //value.append(" # ");
475                 consume('#');
476             }
477             else
478             {
479                 String textToken = parseTextToken();
480                 if (textToken.length() == 0)
481                     throw new IOException("Error in line "+line+" or above: "+
482                                           "Empty text token.\nThis could be caused "+
483                                           "by a missing comma between two fields.");
484             value.append("#").append(textToken).append("#");
485                 //Util.pr(parseTextToken());
486                 //throw new RuntimeException("Unknown field type");
487             }
488             skipWhitespace();
489         }
490         //Util.pr("Returning field content: "+value.toString());
491
492         // Check if we are to strip extra pairs of braces before returning:
493         if (Globals.prefs.getBoolean("autoDoubleBraces")) {
494             // Do it:
495             while ((value.length()>1) && (value.charAt(0) == '{') && (value.charAt(value.length()-1) == '}')) {
496                 value.deleteCharAt(value.length()-1);
497                 value.deleteCharAt(0);
498             }
499             // Problem: if the field content is "{DNA} blahblah {EPA}", one pair too much will be removed.
500             // Check if this is the case, and re-add as many pairs as needed.
501             while (hasNegativeBraceCount(value.toString())) {
502                 value.insert(0, '{');
503                 value.append('}');
504             }
505
506         }
507         return value.toString();
508
509     }
510
511     /**
512      * Check if a string at any point has had more ending braces (}) than opening ones ({).
513      * Will e.g. return true for the string "DNA} blahblal {EPA"
514      * @param s The string to check.
515      * @return true if at any index the brace count is negative.
516      */
517     private boolean hasNegativeBraceCount(String s) {
518         //System.out.println(s);
519         int i=0, count=0;
520         while (i<s.length()) {
521             if (s.charAt(i) == '{')
522                 count++;
523             else if (s.charAt(i) == '}')
524                 count--;
525             if (count < 0)
526                 return true;
527             i++;
528         }
529         return false;
530     }
531
532     /**
533      * This method is used to parse string labels, field names, entry
534      * type and numbers outside brackets.
535      */
536     private String parseTextToken() throws IOException
537     {
538         StringBuffer token = new StringBuffer(20);
539
540         while (true)
541         {
542             int c = read();
543             //Util.pr(".. "+c);
544             if (c == -1)
545             {
546                 _eof = true;
547
548                 return token.toString();
549             }
550
551             if (Character.isLetterOrDigit((char) c) ||
552                 (c == ':') || (c == '-')
553                 || (c == '_') || (c == '*') || (c == '+') || (c == '.')
554                 || (c == '/') || (c == '\''))
555             {
556                 token.append((char) c);
557             }
558             else
559             {
560                 unread(c);
561                 //Util.pr("Pasted text token: "+token.toString());
562                 return token.toString();
563             }
564         }
565     }
566
567     /**
568      * This method is used to parse the bibtex key for an entry.
569      */
570     private String parseKey() throws IOException,NoLabelException
571     {
572        StringBuffer token = new StringBuffer(20);
573
574         while (true)
575         {
576             int c = read();
577             //Util.pr(".. '"+(char)c+"'\t"+c);
578             if (c == -1)
579             {
580                 _eof = true;
581
582                 return token.toString();
583             }
584
585             // Ikke: #{}\uFFFD~\uFFFD
586             //
587             // G\uFFFDr:  $_*+.-\/?"^
588             if (!Character.isWhitespace((char)c) && (Character.isLetterOrDigit((char) c) ||
589                 ((c != '#') && (c != '{') && (c != '}') && (c != '\uFFFD')
590                  && (c != '~') && (c != '\uFFFD') && (c != ',') && (c != '=')
591                  )))
592             {
593                 token.append((char) c);
594             }
595             else
596             {
597
598                 if (Character.isWhitespace((char)c)) {
599                     // We have encountered white space instead of the comma at the end of
600                     // the key. Possibly the comma is missing, so we try to return what we
601                     // have found, as the key.
602                     return token.toString();
603                 }
604                 else if (c == ',') {
605                     unread(c);
606                     return token.toString();
607                     //} else if (Character.isWhitespace((char)c)) {
608                     //throw new NoLabelException(token.toString());
609                 } else if (c == '=') {
610                     // If we find a '=' sign, it is either an error, or
611                     // the entry lacked a comma signifying the end of the key.
612
613                     return token.toString();
614                     //throw new NoLabelException(token.toString());
615
616                 } else
617                     throw new IOException("Error in line "+line+":"+
618                                           "Character '"+(char)c+"' is not "+
619                                           "allowed in bibtex keys.");
620
621             }
622         }
623
624
625     }
626
627     private class NoLabelException extends Exception {
628         public NoLabelException(String hasRead) {
629             super(hasRead);
630         }
631     }
632
633     private StringBuffer parseBracketedText() throws IOException
634     {
635         //Util.pr("Parse bracketed text");
636         StringBuffer value = new StringBuffer();
637
638         consume('{');
639
640         int brackets = 0;
641
642         while (!((peek() == '}') && (brackets == 0)))
643         {
644
645             int j = read();
646             if ((j == -1) || (j == 65535))
647             {
648                 throw new RuntimeException("Error in line "+line
649                                            +": EOF in mid-string");
650             }
651             else if (j == '{')
652                 brackets++;
653             else if (j == '}')
654                 brackets--;
655
656             // If we encounter whitespace of any kind, read it as a
657             // simple space, and ignore any others that follow immediately.
658         /*if (j == '\n') {
659             if (peek() == '\n')
660                 value.append('\n');
661         }
662             else*/ if (Character.isWhitespace((char)j)) {
663             String whs = skipAndRecordWhitespace(j);
664             //System.out.println(":"+whs+":");
665                     if (!whs.equals("") && !whs.equals("\n\t")) { // && !whs.equals("\n"))
666                 whs = whs.replaceAll("\t", ""); // Remove tabulators.
667                 //while (whs.endsWith("\t"))
668                 //    whs = whs.substring(0, whs.length()-1);
669                 value.append(whs);
670             }
671             else
672                 value.append(' ');
673
674
675             } else
676                 value.append((char) j);
677
678         }
679
680         consume('}');
681
682         return value;
683     }
684
685     private StringBuffer parseBracketedTextExactly() throws IOException
686     {
687
688         StringBuffer value = new StringBuffer();
689
690         consume('{');
691
692         int brackets = 0;
693
694         while (!((peek() == '}') && (brackets == 0)))
695         {
696
697             int j = read();
698             if ((j == -1) || (j == 65535))
699             {
700                 throw new RuntimeException("Error in line "+line
701                                            +": EOF in mid-string");
702             }
703             else if (j == '{')
704                 brackets++;
705             else if (j == '}')
706                 brackets--;
707
708             value.append((char) j);
709
710         }
711
712         consume('}');
713
714         return value;
715     }
716
717     private void consume(char expected) throws IOException
718     {
719         int c = read();
720
721         if (c != expected)
722         {
723             throw new RuntimeException("Error in line "+line
724                     +": Expected "
725                     + expected + " but received " + (char) c);
726         }
727
728     }
729
730     private boolean consumeUncritically(char expected) throws IOException
731     {
732         int c;
733         while (((c = read()) != expected) && (c != -1) && (c != 65535));
734         if ((c == -1) || (c == 65535))
735             _eof = true;
736
737         // Return true if we actually found the character we were looking for:
738         return c == expected;
739     }
740
741     private void consume(char expected1, char expected2) throws IOException
742     {
743         // Consumes one of the two, doesn't care which appears.
744
745         int c = read();
746
747         if ((c != expected1) && (c != expected2))
748         {
749             throw new RuntimeException("Error in line "+line+": Expected " +
750                 expected1 + " or " + expected2 + " but received " + (int) c);
751
752         }
753
754     }
755
756     public void checkEntryTypes(ParserResult _pr) {
757         for (Iterator i=_db.getKeySet().iterator(); i.hasNext();) {
758         Object key = i.next();
759             BibtexEntry be = (BibtexEntry)_db.getEntryById((String)key);
760             if (be.getType() instanceof UnknownEntryType) {
761                 // Look up the unknown type name in our map of parsed types:
762
763                 Object o = entryTypes.get(be.getType().getName().toLowerCase());
764                 if (o != null) {
765                     BibtexEntryType type = (BibtexEntryType)o;
766                     be.setType(type);
767                 } else {
768             //System.out.println("Unknown entry type: "+be.getType().getName());
769             _pr.addWarning(Globals.lang("unknown entry type")+": "+be.getType().getName()+". "+
770                  Globals.lang("Type set to 'other'")+".");
771             be.setType(BibtexEntryType.OTHER);
772         }
773             }
774         }
775     }
776 }