fa007457de8a710d9845a4f74dbe80ab3edce655
[debian/jabref.git] / src / main / java / net / sf / jabref / export / FileActions.java
1 /*  Copyright (C) 2003-2011 JabRef contributors.
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.export;
17
18 import java.io.File;
19 import java.io.FileNotFoundException;
20 import java.io.FileReader;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.Reader;
24 import java.io.Writer;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Set;
33 import java.util.TreeMap;
34 import java.util.Vector;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37
38 import net.sf.jabref.BibtexDatabase;
39 import net.sf.jabref.BibtexEntry;
40 import net.sf.jabref.BibtexEntryType;
41 import net.sf.jabref.BibtexFields;
42 import net.sf.jabref.BibtexString;
43 import net.sf.jabref.BibtexStringComparator;
44 import net.sf.jabref.CrossRefEntryComparator;
45 import net.sf.jabref.CustomEntryType;
46 import net.sf.jabref.FieldComparator;
47 import net.sf.jabref.FieldComparatorStack;
48 import net.sf.jabref.GUIGlobals;
49 import net.sf.jabref.Globals;
50 import net.sf.jabref.IdComparator;
51 import net.sf.jabref.JabRefPreferences;
52 import net.sf.jabref.MetaData;
53 import net.sf.jabref.config.SaveOrderConfig;
54 import ca.odell.glazedlists.BasicEventList;
55 import ca.odell.glazedlists.SortedList;
56
57 public class FileActions {
58
59     private static Pattern refPat = Pattern.compile("(#[A-Za-z]+#)"); // Used to detect string references in strings
60     private static BibtexString.Type previousStringType;
61
62     private static void writePreamble(Writer fw, String preamble) throws IOException {
63         if (preamble != null) {
64             fw.write("@PREAMBLE{");
65             fw.write(preamble);
66             fw.write("}" + Globals.NEWLINE + Globals.NEWLINE);
67         }
68     }
69
70     /**
71      * Write all strings in alphabetical order, modified to produce a safe (for
72      * BibTeX) order of the strings if they reference each other.
73      *
74      * @param fw The Writer to send the output to.
75      * @param database The database whose strings we should write.
76      * @throws IOException If anthing goes wrong in writing.
77      */
78     private static void writeStrings(Writer fw, BibtexDatabase database) throws IOException {
79         previousStringType = BibtexString.Type.AUTHOR;
80         List<BibtexString> strings = new ArrayList<BibtexString>();
81         for (String s : database.getStringKeySet()) {
82             strings.add(database.getString(s));
83         }
84         Collections.sort(strings, new BibtexStringComparator(true));
85         // First, make a Map of all entries:
86         HashMap<String, BibtexString> remaining = new HashMap<String, BibtexString>();
87         int maxKeyLength = 0;
88         for (BibtexString string : strings) {
89             remaining.put(string.getName(), string);
90             maxKeyLength = Math.max(maxKeyLength, string.getName().length());
91         }
92
93         for (BibtexString.Type t : BibtexString.Type.values()) {
94             for (BibtexString bs : strings) {
95                 if (remaining.containsKey(bs.getName()) && bs.getType() == t) {
96                     writeString(fw, bs, remaining, maxKeyLength);
97                 }
98             }
99         }
100         fw.write(Globals.NEWLINE);
101     }
102
103     private static void writeString(Writer fw, BibtexString bs, HashMap<String, BibtexString> remaining, int maxKeyLength) throws IOException {
104         // First remove this from the "remaining" list so it can't cause problem with circular refs:
105         remaining.remove(bs.getName());
106
107         // Then we go through the string looking for references to other strings. If we find references
108         // to strings that we will write, but still haven't, we write those before proceeding. This ensures
109         // that the string order will be acceptable for BibTeX.
110         String content = bs.getContent();
111         Matcher m;
112         while ((m = refPat.matcher(content)).find()) {
113             String foundLabel = m.group(1);
114             int restIndex = content.indexOf(foundLabel) + foundLabel.length();
115             content = content.substring(restIndex);
116             Object referred = remaining.get(foundLabel.substring(1, foundLabel.length() - 1));
117             // If the label we found exists as a key in the "remaining" Map, we go on and write it now:
118             if (referred != null) {
119                 writeString(fw, (BibtexString) referred, remaining, maxKeyLength);
120             }
121         }
122
123         if (previousStringType != bs.getType()) {
124             fw.write(Globals.NEWLINE);
125             previousStringType = bs.getType();
126         }
127
128         String suffix = "";
129         for (int i = maxKeyLength - bs.getName().length(); i > 0; i--) {
130             suffix += " ";
131         }
132
133         fw.write("@String { " + bs.getName() + suffix + " = ");
134         if (!bs.getContent().equals("")) {
135             try {
136                 String formatted = (new LatexFieldFormatter()).format(bs.getContent(), Globals.BIBTEX_STRING);
137                 fw.write(formatted);
138             } catch (IllegalArgumentException ex) {
139                 throw new IllegalArgumentException(
140                         Globals.lang("The # character is not allowed in BibTeX strings unless escaped as in '\\#'.") + "\n"
141                         + Globals.lang("Before saving, please edit any strings containing the # character."));
142             }
143
144         } else {
145             fw.write("{}");
146         }
147
148         fw.write(" }" + Globals.NEWLINE);// + Globals.NEWLINE);
149     }
150
151     /**
152      * Writes the JabRef signature and the encoding.
153      *
154      * @param encoding String the name of the encoding, which is part of the
155      * header.
156      */
157     private static void writeBibFileHeader(Writer out, String encoding) throws IOException {
158         out.write("% ");
159         out.write(GUIGlobals.SIGNATURE);
160         out.write(" " + GUIGlobals.version + "." + Globals.NEWLINE + "% "
161                 + GUIGlobals.encPrefix + encoding + Globals.NEWLINE + Globals.NEWLINE);
162     }
163
164     /**
165      * Saves the database to file. Two boolean values indicate whether only
166      * entries with a nonzero Globals.SEARCH value and only entries with a
167      * nonzero Globals.GROUPSEARCH value should be saved. This can be used to
168      * let the user save only the results of a search. False and false means all
169      * entries are saved.
170      */
171     public static SaveSession saveDatabase(BibtexDatabase database,
172             MetaData metaData, File file, JabRefPreferences prefs,
173             boolean checkSearch, boolean checkGroup, String encoding, boolean suppressBackup)
174             throws SaveException {
175
176         TreeMap<String, BibtexEntryType> types = new TreeMap<String, BibtexEntryType>();
177
178         boolean backup = prefs.getBoolean("backup");
179         if (suppressBackup) {
180             backup = false;
181         }
182
183         SaveSession session;
184         BibtexEntry exceptionCause = null;
185         try {
186             session = new SaveSession(file, encoding, backup);
187         } catch (Throwable e) {
188             if (encoding != null) {
189                 System.err.println("Error from encoding: '" + encoding + "' Len: " + encoding.length());
190             }
191                         // we must catch all exceptions to be able notify users that
192             // saving failed, no matter what the reason was
193             // (and they won't just quit JabRef thinking
194             // everyting worked and loosing data)
195             e.printStackTrace();
196             throw new SaveException(e.getMessage());
197         }
198
199         try {
200
201                         // Get our data stream. This stream writes only to a temporary file,
202             // until committed.
203             VerifyingWriter fw = session.getWriter();
204
205             // Write signature.
206             writeBibFileHeader(fw, encoding);
207
208             // Write preamble if there is one.
209             writePreamble(fw, database.getPreamble());
210
211             // Write strings if there are any.
212             writeStrings(fw, database);
213
214                         // Write database entries. Take care, using CrossRefEntry-
215             // Comparator, that referred entries occur after referring
216             // ones. Apart from crossref requirements, entries will be
217             // sorted as they appear on the screen.
218             List<BibtexEntry> sorter = getSortedEntries(database, metaData, null, true);
219
220             FieldFormatter ff = new LatexFieldFormatter();
221
222             for (BibtexEntry be : sorter) {
223                 exceptionCause = be;
224
225                                 // Check if we must write the type definition for this
226                 // entry, as well. Our criterion is that all non-standard
227                 // types (*not* customized standard types) must be written.
228                 BibtexEntryType tp = be.getType();
229
230                 if (BibtexEntryType.getStandardType(tp.getName()) == null) {
231                     types.put(tp.getName(), tp);
232                 }
233
234                 // Check if the entry should be written.
235                 boolean write = true;
236
237                 if (checkSearch && !nonZeroField(be, BibtexFields.SEARCH)) {
238                     write = false;
239                 }
240
241                 if (checkGroup && !nonZeroField(be, BibtexFields.GROUPSEARCH)) {
242                     write = false;
243                 }
244
245                 if (write) {
246                     be.write(fw, ff, true);
247                     fw.write(Globals.NEWLINE);
248                 }
249             }
250
251             // Write meta data.
252             if (metaData != null) {
253                 metaData.writeMetaData(fw);
254             }
255
256             // Write type definitions, if any:
257             if (types.size() > 0) {
258                 for (String s : types.keySet()) {
259                     BibtexEntryType type = types.get(s);
260                     if (type instanceof CustomEntryType) {
261                         CustomEntryType tp = (CustomEntryType) type;
262                         tp.save(fw);
263                         fw.write(Globals.NEWLINE);
264                     }
265                 }
266
267             }
268
269             fw.close();
270         } catch (Throwable ex) {
271             ex.printStackTrace();
272             session.cancel();
273             // repairAfterError(file, backup, INIT_OK);
274             throw new SaveException(ex.getMessage(), exceptionCause);
275         }
276
277         return session;
278
279     }
280
281     private static class SaveSettings {
282         public final String pri, sec, ter;
283         public final boolean priD, secD, terD;
284
285         public SaveSettings(boolean isSaveOperation, MetaData metaData) {
286             /* three options:
287              * 1. original order (saveInOriginalOrder) -- not hit here as SaveSettings is not called in that case
288              * 2. current table sort order
289              * 3. ordered by specified order
290              */
291
292                         Vector<String> storedSaveOrderConfig = null;
293                         if (isSaveOperation) {
294                                 storedSaveOrderConfig = metaData.getData(net.sf.jabref.gui.DatabasePropertiesDialog.SAVE_ORDER_CONFIG);
295                         }
296                         
297                         // This case should never be hit as SaveSettings() is never called if InOriginalOrder is true
298                         assert (storedSaveOrderConfig == null) && isSaveOperation && !Globals.prefs.getBoolean(JabRefPreferences.SAVE_IN_ORIGINAL_ORDER);
299                         assert (storedSaveOrderConfig == null) && !isSaveOperation && !Globals.prefs.getBoolean(JabRefPreferences.EXPORT_IN_ORIGINAL_ORDER);
300
301                         if (storedSaveOrderConfig != null) {
302                                 // follow the metaData
303                                 SaveOrderConfig saveOrderConfig = new SaveOrderConfig(storedSaveOrderConfig);
304                                 assert (!saveOrderConfig.saveInOriginalOrder);
305                                 assert (saveOrderConfig.saveInSpecifiedOrder);
306                                 pri = saveOrderConfig.sortCriteria[0].field;
307                                 sec = saveOrderConfig.sortCriteria[1].field;
308                                 ter = saveOrderConfig.sortCriteria[2].field;
309                                 priD = saveOrderConfig.sortCriteria[0].descending;
310                                 secD = saveOrderConfig.sortCriteria[1].descending;
311                                 terD = saveOrderConfig.sortCriteria[2].descending;
312                         } else if (isSaveOperation && Globals.prefs.getBoolean(JabRefPreferences.SAVE_IN_SPECIFIED_ORDER)) {
313                                 pri = Globals.prefs.get(JabRefPreferences.SAVE_PRIMARY_SORT_FIELD);
314                                 sec = Globals.prefs.get(JabRefPreferences.SAVE_SECONDARY_SORT_FIELD);
315                                 ter = Globals.prefs.get(JabRefPreferences.SAVE_TERTIARY_SORT_FIELD);
316                                 priD = Globals.prefs.getBoolean(JabRefPreferences.SAVE_PRIMARY_SORT_DESCENDING);
317                                 secD = Globals.prefs.getBoolean(JabRefPreferences.SAVE_SECONDARY_SORT_DESCENDING);
318                                 terD = Globals.prefs.getBoolean(JabRefPreferences.SAVE_TERTIARY_SORT_DESCENDING);
319                         } else if (!isSaveOperation && Globals.prefs.getBoolean(JabRefPreferences.EXPORT_IN_SPECIFIED_ORDER)) {
320                                 pri = Globals.prefs.get(JabRefPreferences.EXPORT_PRIMARY_SORT_FIELD);
321                                 sec = Globals.prefs.get(JabRefPreferences.EXPORT_SECONDARY_SORT_FIELD);
322                                 ter = Globals.prefs.get(JabRefPreferences.EXPORT_TERTIARY_SORT_FIELD);
323                                 priD = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_PRIMARY_SORT_DESCENDING);
324                                 secD = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_SECONDARY_SORT_DESCENDING);
325                                 terD = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_TERTIARY_SORT_DESCENDING);
326                         } else {
327                                 // The setting is to save according to the current table order.
328                                 pri = Globals.prefs.get(JabRefPreferences.PRIMARY_SORT_FIELD);
329                                 sec = Globals.prefs.get(JabRefPreferences.SECONDARY_SORT_FIELD);
330                                 ter = Globals.prefs.get(JabRefPreferences.TERTIARY_SORT_FIELD);
331                                 priD = Globals.prefs.getBoolean(JabRefPreferences.PRIMARY_SORT_DESCENDING);
332                                 secD = Globals.prefs.getBoolean(JabRefPreferences.SECONDARY_SORT_DESCENDING);
333                                 terD = Globals.prefs.getBoolean(JabRefPreferences.TERTIARY_SORT_DESCENDING);
334                         }
335         }
336     }
337
338     private static List<Comparator<BibtexEntry>> getSaveComparators(boolean isSaveOperation, MetaData metaData) {
339         SaveSettings saveSettings = new SaveSettings(isSaveOperation, metaData);
340
341         List<Comparator<BibtexEntry>> comparators = new ArrayList<Comparator<BibtexEntry>>();
342         if (isSaveOperation) {
343             comparators.add(new CrossRefEntryComparator());
344         }
345         comparators.add(new FieldComparator(saveSettings.pri, saveSettings.priD));
346         comparators.add(new FieldComparator(saveSettings.sec, saveSettings.secD));
347         comparators.add(new FieldComparator(saveSettings.ter, saveSettings.terD));
348         comparators.add(new FieldComparator(BibtexFields.KEY_FIELD));
349
350         return comparators;
351     }
352
353     /**
354      * Saves the database to file, including only the entries included in the
355      * supplied input array bes.
356      *
357      * @return A List containing warnings, if any.
358      */
359     public static SaveSession savePartOfDatabase(BibtexDatabase database, MetaData metaData,
360             File file, JabRefPreferences prefs, BibtexEntry[] bes, String encoding) throws SaveException {
361
362         TreeMap<String, BibtexEntryType> types = new TreeMap<String, BibtexEntryType>(); // Map
363         // to
364         // collect
365         // entry
366         // type
367         // definitions
368         // that we must save along with entries using them.
369
370         BibtexEntry be = null;
371         boolean backup = prefs.getBoolean("backup");
372
373         SaveSession session;
374         try {
375             session = new SaveSession(file, encoding, backup);
376         } catch (IOException e) {
377             throw new SaveException(e.getMessage());
378         }
379
380         try {
381
382             // Define our data stream.
383             VerifyingWriter fw = session.getWriter();
384
385             // Write signature.
386             writeBibFileHeader(fw, encoding);
387
388             // Write preamble if there is one.
389             writePreamble(fw, database.getPreamble());
390
391             // Write strings if there are any.
392             writeStrings(fw, database);
393
394             // Write database entries. Take care, using CrossRefEntry-
395             // Comparator, that referred entries occur after referring
396             // ones. Apart from crossref requirements, entries will be
397             // sorted as they appear on the screen.
398             List<Comparator<BibtexEntry>> comparators = getSaveComparators(true, metaData);
399
400             // Use glazed lists to get a sorted view of the entries:
401             BasicEventList<BibtexEntry> entryList = new BasicEventList<BibtexEntry>();
402             SortedList<BibtexEntry> sorter = new SortedList<BibtexEntry>(entryList, new FieldComparatorStack<BibtexEntry>(comparators));
403
404             if ((bes != null) && (bes.length > 0)) {
405                 Collections.addAll(sorter, bes);
406             }
407
408             FieldFormatter ff = new LatexFieldFormatter();
409
410             for (BibtexEntry aSorter : sorter) {
411                 be = (aSorter);
412
413                 // Check if we must write the type definition for this
414                 // entry, as well. Our criterion is that all non-standard
415                 // types (*not* customized standard types) must be written.
416                 BibtexEntryType tp = be.getType();
417                 if (BibtexEntryType.getStandardType(tp.getName()) == null) {
418                     types.put(tp.getName(), tp);
419                 }
420
421                 be.write(fw, ff, true);
422                 fw.write(Globals.NEWLINE);
423             }
424
425             // Write meta data.
426             if (metaData != null) {
427                 metaData.writeMetaData(fw);
428             }
429
430             // Write type definitions, if any:
431             if (types.size() > 0) {
432                 for (String s : types.keySet()) {
433                     CustomEntryType tp = (CustomEntryType) types.get(s);
434                     tp.save(fw);
435                     fw.write(Globals.NEWLINE);
436                 }
437
438             }
439
440             fw.close();
441         } catch (Throwable ex) {
442             session.cancel();
443             //repairAfterError(file, backup, status);
444             throw new SaveException(ex.getMessage(), be);
445         }
446
447         return session;
448
449     }
450
451     /**
452      * This method attempts to get a Reader for the file path given, either by
453      * loading it as a resource (from within jar), or as a normal file. If
454      * unsuccessful (e.g. file not found), an IOException is thrown.
455      */
456     public static Reader getReader(String name) throws IOException {
457         Reader reader = null;
458         // Try loading as a resource first. This works for files inside the jar:
459         URL reso = Globals.class.getResource(name);
460
461         // If that didn't work, try loading as a normal file URL:
462         if (reso != null) {
463             try {
464                 reader = new InputStreamReader(reso.openStream());
465             } catch (FileNotFoundException ex) {
466                 throw new IOException(Globals.lang("Could not find layout file") + ": '" + name + "'.");
467             }
468         } else {
469             File f = new File(name);
470             try {
471                 reader = new FileReader(f);
472             } catch (FileNotFoundException ex) {
473                 throw new IOException(Globals.lang("Could not find layout file") + ": '" + name + "'.");
474             }
475         }
476
477         return reader;
478     }
479
480     /*
481      * We have begun to use getSortedEntries() for both database save operations
482      * and non-database save operations.  In a non-database save operation
483      * (such as the exportDatabase call), we do not wish to use the
484      * global preference of saving in standard order.
485      */
486     @SuppressWarnings("unchecked")
487     public static List<BibtexEntry> getSortedEntries(BibtexDatabase database, MetaData metaData, Set<String> keySet, boolean isSaveOperation) {
488         boolean inOriginalOrder;
489         if (isSaveOperation) {
490                         Vector<String> storedSaveOrderConfig = metaData.getData(net.sf.jabref.gui.DatabasePropertiesDialog.SAVE_ORDER_CONFIG);
491                         if (storedSaveOrderConfig == null) {
492                                 inOriginalOrder = Globals.prefs.getBoolean("saveInOriginalOrder");
493                         } else {
494                                 SaveOrderConfig saveOrderConfig = new SaveOrderConfig(storedSaveOrderConfig);
495                                 inOriginalOrder = saveOrderConfig.saveInOriginalOrder;
496                         }
497                 } else {
498                         inOriginalOrder = Globals.prefs.getBoolean("exportInOriginalOrder");
499                 }
500         List<Comparator<BibtexEntry>> comparators;
501         if (inOriginalOrder) {
502             // Sort entries based on their creation order, utilizing the fact
503             // that IDs used for entries are increasing, sortable numbers.
504             comparators = new ArrayList<Comparator<BibtexEntry>>();
505             comparators.add(new CrossRefEntryComparator());
506             comparators.add(new IdComparator());
507         } else {
508             comparators = getSaveComparators(isSaveOperation, metaData);
509         }
510
511         // Use glazed lists to get a sorted view of the entries:
512         FieldComparatorStack<BibtexEntry> comparatorStack = new FieldComparatorStack<BibtexEntry>(comparators);
513         BasicEventList<BibtexEntry> entryList = new BasicEventList<BibtexEntry>();
514         SortedList<BibtexEntry> sorter = new SortedList<BibtexEntry>(entryList, comparatorStack);
515
516         if (keySet == null) {
517             keySet = database.getKeySet();
518         }
519
520         if (keySet != null) {
521             Iterator<String> i = keySet.iterator();
522
523             for (; i.hasNext();) {
524                 sorter.add(database.getEntryById((i.next())));
525             }
526         }
527         return sorter;
528     }
529
530     /**
531      * @return true iff the entry has a nonzero value in its field.
532      */
533     private static boolean nonZeroField(BibtexEntry be, String field) {
534         String o = (be.getField(field));
535
536         return ((o != null) && !o.equals("0"));
537     }
538 }
539
540 ///////////////////////////////////////////////////////////////////////////////
541 //  END OF FILE.
542 ///////////////////////////////////////////////////////////////////////////////