/* Copyright (C) 2003-2011 JabRef contributors. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package net.sf.jabref.export; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sf.jabref.BibtexDatabase; import net.sf.jabref.BibtexEntry; import net.sf.jabref.BibtexEntryType; import net.sf.jabref.BibtexFields; import net.sf.jabref.BibtexString; import net.sf.jabref.BibtexStringComparator; import net.sf.jabref.CrossRefEntryComparator; import net.sf.jabref.CustomEntryType; import net.sf.jabref.FieldComparator; import net.sf.jabref.FieldComparatorStack; import net.sf.jabref.GUIGlobals; import net.sf.jabref.Globals; import net.sf.jabref.IdComparator; import net.sf.jabref.JabRefPreferences; import net.sf.jabref.MetaData; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.SortedList; public class FileActions { private static Pattern refPat = Pattern.compile("(#[A-Za-z]+#)"); // Used to detect string references in strings private static void writePreamble(Writer fw, String preamble) throws IOException { if (preamble != null) { fw.write("@PREAMBLE{"); fw.write(preamble); fw.write("}"+Globals.NEWLINE +Globals.NEWLINE); } } /** * Write all strings in alphabetical order, modified to produce a safe (for BibTeX) order of the strings * if they reference each other. * @param fw The Writer to send the output to. * @param database The database whose strings we should write. * @throws IOException If anthing goes wrong in writing. */ private static void writeStrings(Writer fw, BibtexDatabase database) throws IOException { List<BibtexString> strings = new ArrayList<BibtexString>(); for (String s : database.getStringKeySet()) { strings.add(database.getString(s)); } Collections.sort(strings, new BibtexStringComparator(true)); // First, make a Map of all entries: HashMap<String, BibtexString> remaining = new HashMap<String, BibtexString>(); for (Iterator<BibtexString> i=strings.iterator(); i.hasNext();) { BibtexString string = i.next(); remaining.put(string.getName(), string); } for (Iterator<BibtexString> i = strings.iterator(); i.hasNext();) { BibtexString bs = i.next(); if (remaining.containsKey(bs.getName())) writeString(fw, bs, remaining); } } private static void writeString(Writer fw, BibtexString bs, HashMap<String, BibtexString> remaining) throws IOException { // First remove this from the "remaining" list so it can't cause problem with circular refs: remaining.remove(bs.getName()); // Then we go through the string looking for references to other strings. If we find references // to strings that we will write, but still haven't, we write those before proceeding. This ensures // that the string order will be acceptable for BibTeX. String content = bs.getContent(); Matcher m; while ((m = refPat.matcher(content)).find()) { String foundLabel = m.group(1); int restIndex = content.indexOf(foundLabel)+foundLabel.length(); content = content.substring(restIndex); Object referred = remaining.get(foundLabel.substring(1, foundLabel.length()-1)); // If the label we found exists as a key in the "remaining" Map, we go on and write it now: if (referred != null) writeString(fw, (BibtexString)referred, remaining); } fw.write("@STRING{" + bs.getName() + " = "); if (!bs.getContent().equals("")) { try { String formatted = (new LatexFieldFormatter()).format(bs.getContent(), Globals.BIBTEX_STRING); fw.write(formatted); } catch (IllegalArgumentException ex) { throw new IllegalArgumentException( Globals.lang("The # character is not allowed in BibTeX strings unless escaped as in '\\#'.") + "\n" + Globals.lang("Before saving, please edit any strings containing the # character.")); } } else fw.write("{}"); fw.write("}" + Globals.NEWLINE + Globals.NEWLINE); } /** * Writes the JabRef signature and the encoding. * * @param encoding String the name of the encoding, which is part of the header. */ private static void writeBibFileHeader(Writer out, String encoding) throws IOException { out.write("% "); out.write(GUIGlobals.SIGNATURE); out.write(" "+GUIGlobals.version+"."+Globals.NEWLINE + "% " + GUIGlobals.encPrefix+encoding+Globals.NEWLINE +Globals.NEWLINE); } /** * Saves the database to file. Two boolean values indicate whether * only entries with a nonzero Globals.SEARCH value and only * entries with a nonzero Globals.GROUPSEARCH value should be * saved. This can be used to let the user save only the results of * a search. False and false means all entries are saved. */ public static SaveSession saveDatabase(BibtexDatabase database, MetaData metaData, File file, JabRefPreferences prefs, boolean checkSearch, boolean checkGroup, String encoding, boolean suppressBackup) throws SaveException { TreeMap<String, BibtexEntryType> types = new TreeMap<String, BibtexEntryType>(); boolean backup = prefs.getBoolean("backup"); if (suppressBackup) backup = false; SaveSession session; BibtexEntry exceptionCause = null; try { session = new SaveSession(file, encoding, backup); } catch (Throwable e) { if (encoding != null) { System.err.println("Error from encoding: '" + encoding + "' Len: " + encoding.length()); } // we must catch all exceptions to be able notify users that // saving failed, no matter what the reason was // (and they won't just quit JabRef thinking // everyting worked and loosing data) e.printStackTrace(); throw new SaveException(e.getMessage()); } try { // Get our data stream. This stream writes only to a temporary file, // until committed. VerifyingWriter fw = session.getWriter(); // Write signature. writeBibFileHeader(fw, encoding); // Write preamble if there is one. writePreamble(fw, database.getPreamble()); // Write strings if there are any. writeStrings(fw, database); // Write database entries. Take care, using CrossRefEntry- // Comparator, that referred entries occur after referring // ones. Apart from crossref requirements, entries will be // sorted as they appear on the screen. List<BibtexEntry> sorter = getSortedEntries(database, null, true); FieldFormatter ff = new LatexFieldFormatter(); for (BibtexEntry be : sorter) { exceptionCause = be; // Check if we must write the type definition for this // entry, as well. Our criterion is that all non-standard // types (*not* customized standard types) must be written. BibtexEntryType tp = be.getType(); if (BibtexEntryType.getStandardType(tp.getName()) == null) { types.put(tp.getName(), tp); } // Check if the entry should be written. boolean write = true; if (checkSearch && !nonZeroField(be, BibtexFields.SEARCH)) { write = false; } if (checkGroup && !nonZeroField(be, BibtexFields.GROUPSEARCH)) { write = false; } if (write) { be.write(fw, ff, true); fw.write(Globals.NEWLINE); } } // Write meta data. if (metaData != null) { metaData.writeMetaData(fw); } // Write type definitions, if any: if (types.size() > 0) { for (Iterator<String> i = types.keySet().iterator(); i .hasNext();) { BibtexEntryType type = types.get(i.next()); if (type instanceof CustomEntryType) { CustomEntryType tp = (CustomEntryType)type; tp.save(fw); fw.write(Globals.NEWLINE); } } } fw.close(); } catch (Throwable ex) { ex.printStackTrace(); try { session.cancel(); // repairAfterError(file, backup, INIT_OK); } catch (IOException e) { // Argh, another error? Can we do anything? e.printStackTrace(); throw new SaveException(ex.getMessage()+"\n"+ Globals.lang("Warning: could not complete file repair; your file may " +"have been corrupted. Error message")+": "+e.getMessage()); } throw new SaveException(ex.getMessage(), exceptionCause); } return session; } /** * Saves the database to file, including only the entries included in the * supplied input array bes. * * @return A List containing warnings, if any. */ @SuppressWarnings("unchecked") public static SaveSession savePartOfDatabase(BibtexDatabase database, MetaData metaData, File file, JabRefPreferences prefs, BibtexEntry[] bes, String encoding) throws SaveException { TreeMap<String, BibtexEntryType> types = new TreeMap<String, BibtexEntryType>(); // Map // to // collect // entry // type // definitions // that we must save along with entries using them. BibtexEntry be = null; boolean backup = prefs.getBoolean("backup"); SaveSession session; try { session = new SaveSession(file, encoding, backup); } catch (IOException e) { throw new SaveException(e.getMessage()); } try { // Define our data stream. VerifyingWriter fw = session.getWriter(); // Write signature. writeBibFileHeader(fw, encoding); // Write preamble if there is one. writePreamble(fw, database.getPreamble()); // Write strings if there are any. writeStrings(fw, database); // Write database entries. Take care, using CrossRefEntry- // Comparator, that referred entries occur after referring // ones. Apart from crossref requirements, entries will be // sorted as they appear on the screen. String pri, sec, ter; boolean priD, secD, terD; if (!prefs.getBoolean("saveInStandardOrder")) { // The setting is to save according to the current table order. pri = prefs.get("priSort"); sec = prefs.get("secSort"); // sorted as they appear on the screen. ter = prefs.get("terSort"); priD = prefs.getBoolean("priDescending"); secD = prefs.getBoolean("secDescending"); terD = prefs.getBoolean("terDescending"); } else { // The setting is to save in standard order: author, editor, year pri = "author"; sec = "editor"; ter = "year"; priD = false; secD = false; terD = true; } List<Comparator<BibtexEntry>> comparators = new ArrayList<Comparator<BibtexEntry>>(); comparators.add(new CrossRefEntryComparator()); comparators.add(new FieldComparator(pri, priD)); comparators.add(new FieldComparator(sec, secD)); comparators.add(new FieldComparator(ter, terD)); comparators.add(new FieldComparator(BibtexFields.KEY_FIELD)); // Use glazed lists to get a sorted view of the entries: BasicEventList entryList = new BasicEventList(); SortedList sorter = new SortedList(entryList, new FieldComparatorStack<BibtexEntry>(comparators)); if ((bes != null) && (bes.length > 0)) for (int i=0; i<bes.length; i++) { sorter.add(bes[i]); } FieldFormatter ff = new LatexFieldFormatter(); for (Iterator<BibtexEntry> i = sorter.iterator(); i.hasNext();) { be = (i.next()); // Check if we must write the type definition for this // entry, as well. Our criterion is that all non-standard // types (*not* customized standard types) must be written. BibtexEntryType tp = be.getType(); if (BibtexEntryType.getStandardType(tp.getName()) == null) { types.put(tp.getName(), tp); } be.write(fw, ff, true); fw.write(Globals.NEWLINE); } // Write meta data. if (metaData != null) { metaData.writeMetaData(fw); } // Write type definitions, if any: if (types.size() > 0) { for (Iterator<String> i=types.keySet().iterator(); i.hasNext();) { CustomEntryType tp = (CustomEntryType)types.get(i.next()); tp.save(fw); fw.write(Globals.NEWLINE); } } fw.close(); } catch (Throwable ex) { try { session.cancel(); //repairAfterError(file, backup, status); } catch (IOException e) { // Argh, another error? Can we do anything? e.printStackTrace(); throw new SaveException(ex.getMessage()+"\n"+ Globals.lang("Warning: could not complete file repair; your file may " +"have been corrupted. Error message")+": "+e.getMessage()); } throw new SaveException(ex.getMessage(), be); } return session; } /** * This method attempts to get a Reader for the file path given, either by * loading it as a resource (from within jar), or as a normal file. * If unsuccessful (e.g. file not found), an IOException is thrown. */ public static Reader getReader(String name) throws IOException { Reader reader = null; // Try loading as a resource first. This works for files inside the jar: URL reso = Globals.class.getResource(name); // If that didn't work, try loading as a normal file URL: if (reso != null) { try { reader = new InputStreamReader(reso.openStream()); } catch (FileNotFoundException ex) { throw new IOException(Globals.lang("Could not find layout file")+": '"+name+"'."); } } else { File f = new File(name); try { reader = new FileReader(f); } catch (FileNotFoundException ex) { throw new IOException(Globals.lang("Could not find layout file")+": '"+name+"'."); } } return reader; } /* * We have begun to use getSortedEntries() for both database save operations * and non-database save operations. In a non-database save operation * (such as the exportDatabase call), we do not wish to use the * global preference of saving in standard order. */ @SuppressWarnings("unchecked") public static List<BibtexEntry> getSortedEntries(BibtexDatabase database, Set<String> keySet, boolean isSaveOperation) { FieldComparatorStack<BibtexEntry> comparatorStack = null; boolean inOriginalOrder = isSaveOperation ? Globals.prefs.getBoolean("saveInOriginalOrder") : Globals.prefs.getBoolean("exportInOriginalOrder"); if (inOriginalOrder) { // Sort entries based on their creation order, utilizing the fact // that IDs used for entries are increasing, sortable numbers. List<Comparator<BibtexEntry>> comparators = new ArrayList<Comparator<BibtexEntry>>(); comparators.add(new CrossRefEntryComparator()); comparators.add(new IdComparator()); comparatorStack = new FieldComparatorStack<BibtexEntry>(comparators); } else { String pri, sec, ter; boolean priD, secD, terD = false; boolean inStandardOrder = isSaveOperation ? Globals.prefs.getBoolean("saveInStandardOrder") : Globals.prefs.getBoolean("exportInStandardOrder"); if (!inStandardOrder) { // The setting is to save according to the current table order. pri = Globals.prefs.get("priSort"); sec = Globals.prefs.get("secSort"); // sorted as they appear on the screen. ter = Globals.prefs.get("terSort"); priD = Globals.prefs.getBoolean("priDescending"); secD = Globals.prefs.getBoolean("secDescending"); terD = Globals.prefs.getBoolean("terDescending"); } else { // The setting is to save in standard order: author, editor, year pri = "author"; sec = "editor"; ter = "year"; priD = false; secD = false; terD = true; } List<Comparator<BibtexEntry>> comparators = new ArrayList<Comparator<BibtexEntry>>(); if (isSaveOperation) comparators.add(new CrossRefEntryComparator()); comparators.add(new FieldComparator(pri, priD)); comparators.add(new FieldComparator(sec, secD)); comparators.add(new FieldComparator(ter, terD)); comparators.add(new FieldComparator(BibtexFields.KEY_FIELD)); comparatorStack = new FieldComparatorStack<BibtexEntry>(comparators); } // Use glazed lists to get a sorted view of the entries: BasicEventList entryList = new BasicEventList(); SortedList sorter = new SortedList(entryList, comparatorStack); if (keySet == null) keySet = database.getKeySet(); if (keySet != null) { Iterator<String> i = keySet.iterator(); for (; i.hasNext();) { sorter.add(database.getEntryById((i.next()))); } } return sorter; } /** Returns true iff the entry has a nonzero value in its field. */ private static boolean nonZeroField(BibtexEntry be, String field) { String o = (be.getField(field)); return ((o != null) && !o.equals("0")); } } /////////////////////////////////////////////////////////////////////////////// // END OF FILE. ///////////////////////////////////////////////////////////////////////////////