net.sf.jabref.exporter.FileActions.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jabref.exporter.FileActions.java

Source

/*  Copyright (C) 2003-2015 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.exporter;

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.nio.charset.Charset;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.jabref.bibtex.EntryTypes;
import net.sf.jabref.logic.CustomEntryTypesManager;
import net.sf.jabref.model.entry.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import net.sf.jabref.*;
import net.sf.jabref.gui.BibtexFields;
import net.sf.jabref.bibtex.BibEntryWriter;
import net.sf.jabref.bibtex.comparator.BibtexStringComparator;
import net.sf.jabref.bibtex.comparator.CrossRefEntryComparator;
import net.sf.jabref.bibtex.comparator.FieldComparator;
import net.sf.jabref.bibtex.comparator.FieldComparatorStack;
import net.sf.jabref.logic.config.SaveOrderConfig;
import net.sf.jabref.logic.id.IdComparator;
import net.sf.jabref.model.database.BibDatabase;

public class FileActions {

    public enum DatabaseSaveType {
        DEFAULT, PLAIN_BIBTEX
    }

    private static final Pattern REFERENCE_PATTERN = Pattern.compile("(#[A-Za-z]+#)"); // Used to detect string references in strings
    private static BibtexString.Type previousStringType;

    private static final Log LOGGER = LogFactory.getLog(FileActions.class);

    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, BibDatabase database) throws IOException {
        FileActions.previousStringType = BibtexString.Type.AUTHOR;
        List<BibtexString> strings = new ArrayList<>();
        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<>();
        int maxKeyLength = 0;
        for (BibtexString string : strings) {
            remaining.put(string.getName(), string);
            maxKeyLength = Math.max(maxKeyLength, string.getName().length());
        }

        for (BibtexString.Type t : BibtexString.Type.values()) {
            for (BibtexString bs : strings) {
                if (remaining.containsKey(bs.getName()) && (bs.getType() == t)) {
                    FileActions.writeString(fw, bs, remaining, maxKeyLength);
                }
            }
        }
    }

    private static void writeString(Writer fw, BibtexString bs, Map<String, BibtexString> remaining,
            int maxKeyLength) throws IOException {
        // First remove this from the "remaining" list so it can't cause problem with circular refs:
        remaining.remove(bs.getName());

        //if the string has not been modified, write it back as it was
        if (!bs.hasChanged()) {
            fw.write(bs.getParsedSerialization());
            return;
        }

        // 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 = FileActions.REFERENCE_PATTERN.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) {
                FileActions.writeString(fw, (BibtexString) referred, remaining, maxKeyLength);
            }
        }

        if (FileActions.previousStringType != bs.getType()) {
            fw.write(Globals.NEWLINE);
            FileActions.previousStringType = bs.getType();
        }

        StringBuilder suffixSB = new StringBuilder();
        for (int i = maxKeyLength - bs.getName().length(); i > 0; i--) {
            suffixSB.append(' ');
        }
        String suffix = suffixSB.toString();

        fw.write("@String { " + bs.getName() + suffix + " = ");
        if (!bs.getContent().isEmpty()) {
            try {
                String formatted = new LatexFieldFormatter().format(bs.getContent(),
                        LatexFieldFormatter.BIBTEX_STRING);
                fw.write(formatted);
            } catch (IllegalArgumentException ex) {
                throw new IllegalArgumentException(
                        "The # character is not allowed in BibTeX strings unless escaped as in '\\#'.\n"
                                + "Before saving, please edit any strings containing the # character.");
            }

        } else {
            fw.write("{}");
        }

        fw.write(" }" + Globals.NEWLINE);
    }

    /**
     * Writes the file encoding information.
     *
     * @param encoding String the name of the encoding, which is part of the file header.
     */
    private static void writeBibFileHeader(Writer out, Charset encoding) throws IOException {
        out.write("% ");
        out.write(Globals.encPrefix + encoding);
    }

    /**
     * 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(BibDatabase database, MetaData metaData, File file,
            JabRefPreferences prefs, boolean checkSearch, boolean checkGroup, Charset encoding,
            boolean suppressBackup) throws SaveException {

        boolean backup = prefs.getBoolean(JabRefPreferences.BACKUP);
        if (suppressBackup) {
            backup = false;
        }

        SaveSession session;
        BibEntry exceptionCause = null;
        try {
            session = new SaveSession(file, encoding, backup);
        } catch (Throwable e) {
            if (encoding != null) {
                LOGGER.error("Error from encoding: '" + encoding.displayName(), e);
            }
            // 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
            // everything worked and loosing data)
            throw new SaveException(e.getMessage(), e.getLocalizedMessage());
        }

        Map<String, EntryType> types = new TreeMap<>();

        // Get our data stream. This stream writes only to a temporary file,
        // until committed.
        try (VerifyingWriter writer = session.getWriter()) {

            // Write signature.
            FileActions.writeBibFileHeader(writer, encoding);

            // Write preamble if there is one.
            FileActions.writePreamble(writer, database.getPreamble());

            // Write strings if there are any.
            FileActions.writeStrings(writer, 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<BibEntry> sorter = FileActions.getSortedEntries(database, metaData, null, true);

            BibEntryWriter bibtexEntryWriter = new BibEntryWriter(new LatexFieldFormatter(), true);

            for (BibEntry entry : sorter) {
                exceptionCause = entry;

                // 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.
                EntryType entryType = entry.getType();

                if (EntryTypes.getStandardType(entryType.getName()) == null) {
                    types.put(entryType.getName(), entryType);
                }

                // Check if the entry should be written.
                boolean write = true;

                if (checkSearch && !FileActions.nonZeroField(entry, BibtexFields.SEARCH)) {
                    write = false;
                }

                if (checkGroup && !FileActions.nonZeroField(entry, BibtexFields.GROUPSEARCH)) {
                    write = false;
                }

                if (write) {
                    bibtexEntryWriter.write(entry, writer);
                }
            }

            // Write meta data.
            if (metaData != null) {
                metaData.writeMetaData(writer);
            }

            // Write type definitions, if any:
            if (!types.isEmpty()) {
                for (Map.Entry<String, EntryType> stringBibtexEntryTypeEntry : types.entrySet()) {
                    EntryType type = stringBibtexEntryTypeEntry.getValue();
                    if (type instanceof CustomEntryType) {
                        CustomEntryType tp = (CustomEntryType) type;
                        CustomEntryTypesManager.save(tp, writer);
                    }
                }

            }

            //finally write whatever remains of the file, but at least a concluding newline
            if ((database.getEpilog() != null) && !(database.getEpilog().isEmpty())) {
                writer.write(database.getEpilog());
            } else {
                writer.write(Globals.NEWLINE);
            }
        } catch (IOException ex) {
            LOGGER.error("Could not write file", ex);
            session.cancel();
            // repairAfterError(file, backup, INIT_OK);
            throw new SaveException(ex.getMessage(), ex.getLocalizedMessage(), exceptionCause);
        }

        return session;

    }

    private static class SaveSettings {

        public final String pri;
        public final String sec;
        public final String ter;
        public final boolean priD;
        public final boolean secD;
        public final boolean terD;

        public SaveSettings(boolean isSaveOperation, MetaData metaData) {
            /* three options:
             * 1. original order (saveInOriginalOrder) -- not hit here as SaveSettings is not called in that case
             * 2. current table sort order
             * 3. ordered by specified order
             */

            Vector<String> storedSaveOrderConfig = null;
            if (isSaveOperation) {
                storedSaveOrderConfig = metaData
                        .getData(net.sf.jabref.gui.DatabasePropertiesDialog.SAVE_ORDER_CONFIG);
            }

            // This case should never be hit as SaveSettings() is never called if InOriginalOrder is true
            assert (storedSaveOrderConfig == null) && isSaveOperation
                    && !Globals.prefs.getBoolean(JabRefPreferences.SAVE_IN_ORIGINAL_ORDER);
            assert (storedSaveOrderConfig == null) && !isSaveOperation
                    && !Globals.prefs.getBoolean(JabRefPreferences.EXPORT_IN_ORIGINAL_ORDER);

            if (storedSaveOrderConfig != null) {
                // follow the metaData
                SaveOrderConfig saveOrderConfig = new SaveOrderConfig(storedSaveOrderConfig);
                assert !saveOrderConfig.saveInOriginalOrder;
                assert saveOrderConfig.saveInSpecifiedOrder;
                pri = saveOrderConfig.sortCriteria[0].field;
                sec = saveOrderConfig.sortCriteria[1].field;
                ter = saveOrderConfig.sortCriteria[2].field;
                priD = saveOrderConfig.sortCriteria[0].descending;
                secD = saveOrderConfig.sortCriteria[1].descending;
                terD = saveOrderConfig.sortCriteria[2].descending;
            } else if (isSaveOperation && Globals.prefs.getBoolean(JabRefPreferences.SAVE_IN_SPECIFIED_ORDER)) {
                pri = Globals.prefs.get(JabRefPreferences.SAVE_PRIMARY_SORT_FIELD);
                sec = Globals.prefs.get(JabRefPreferences.SAVE_SECONDARY_SORT_FIELD);
                ter = Globals.prefs.get(JabRefPreferences.SAVE_TERTIARY_SORT_FIELD);
                priD = Globals.prefs.getBoolean(JabRefPreferences.SAVE_PRIMARY_SORT_DESCENDING);
                secD = Globals.prefs.getBoolean(JabRefPreferences.SAVE_SECONDARY_SORT_DESCENDING);
                terD = Globals.prefs.getBoolean(JabRefPreferences.SAVE_TERTIARY_SORT_DESCENDING);
            } else if (!isSaveOperation && Globals.prefs.getBoolean(JabRefPreferences.EXPORT_IN_SPECIFIED_ORDER)) {
                pri = Globals.prefs.get(JabRefPreferences.EXPORT_PRIMARY_SORT_FIELD);
                sec = Globals.prefs.get(JabRefPreferences.EXPORT_SECONDARY_SORT_FIELD);
                ter = Globals.prefs.get(JabRefPreferences.EXPORT_TERTIARY_SORT_FIELD);
                priD = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_PRIMARY_SORT_DESCENDING);
                secD = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_SECONDARY_SORT_DESCENDING);
                terD = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_TERTIARY_SORT_DESCENDING);
            } else {
                // The setting is to save according to the current table order.
                pri = Globals.prefs.get(JabRefPreferences.TABLE_PRIMARY_SORT_FIELD);
                sec = Globals.prefs.get(JabRefPreferences.TABLE_SECONDARY_SORT_FIELD);
                ter = Globals.prefs.get(JabRefPreferences.TABLE_TERTIARY_SORT_FIELD);
                priD = Globals.prefs.getBoolean(JabRefPreferences.TABLE_PRIMARY_SORT_DESCENDING);
                secD = Globals.prefs.getBoolean(JabRefPreferences.TABLE_SECONDARY_SORT_DESCENDING);
                terD = Globals.prefs.getBoolean(JabRefPreferences.TABLE_TERTIARY_SORT_DESCENDING);
            }
        }
    }

    private static List<Comparator<BibEntry>> getSaveComparators(boolean isSaveOperation, MetaData metaData) {
        SaveSettings saveSettings = new SaveSettings(isSaveOperation, metaData);

        List<Comparator<BibEntry>> comparators = new ArrayList<>();
        if (isSaveOperation) {
            comparators.add(new CrossRefEntryComparator());
        }
        comparators.add(new FieldComparator(saveSettings.pri, saveSettings.priD));
        comparators.add(new FieldComparator(saveSettings.sec, saveSettings.secD));
        comparators.add(new FieldComparator(saveSettings.ter, saveSettings.terD));
        comparators.add(new FieldComparator(BibEntry.KEY_FIELD));

        return comparators;
    }

    /**
     * Saves the database to file, including only the entries included in the
     * supplied input array bes.
     *
     * @return A List containing warnings, if any.
     */
    public static SaveSession savePartOfDatabase(BibDatabase database, MetaData metaData, File file,
            JabRefPreferences prefs, BibEntry[] bes, Charset encoding, DatabaseSaveType saveType)
            throws SaveException {

        BibEntry be = null;
        boolean backup = prefs.getBoolean(JabRefPreferences.BACKUP);

        SaveSession session;
        try {
            session = new SaveSession(file, encoding, backup);
        } catch (IOException e) {
            throw new SaveException(e.getMessage(), e.getLocalizedMessage());
        }

        // Map to collect entry type definitions
        // that we must save along with entries using them.
        Map<String, EntryType> types = new TreeMap<>();

        // Define our data stream.
        try (VerifyingWriter fw = session.getWriter()) {

            if (saveType != DatabaseSaveType.PLAIN_BIBTEX) {
                // Write signature.
                FileActions.writeBibFileHeader(fw, encoding);
            }

            // Write preamble if there is one.
            FileActions.writePreamble(fw, database.getPreamble());

            // Write strings if there are any.
            FileActions.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<Comparator<BibEntry>> comparators = FileActions.getSaveComparators(true, metaData);

            // Use glazed lists to get a sorted view of the entries:
            List<BibEntry> sorter = new ArrayList<>(bes.length);
            Collections.addAll(sorter, bes);
            Collections.sort(sorter, new FieldComparatorStack<>(comparators));

            BibEntryWriter bibtexEntryWriter = new BibEntryWriter(new LatexFieldFormatter(), true);

            for (BibEntry aSorter : sorter) {
                be = aSorter;

                // 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.
                EntryType tp = be.getType();
                if (EntryTypes.getStandardType(tp.getName()) == null) {
                    types.put(tp.getName(), tp);
                }

                bibtexEntryWriter.write(be, fw);
                //only append newline if the entry has changed
                if (!be.hasChanged()) {
                    fw.write(Globals.NEWLINE);
                }
            }

            // Write meta data.
            if ((saveType != DatabaseSaveType.PLAIN_BIBTEX) && (metaData != null)) {
                metaData.writeMetaData(fw);
            }

            // Write type definitions, if any:
            if (!types.isEmpty()) {
                for (Map.Entry<String, EntryType> stringBibtexEntryTypeEntry : types.entrySet()) {
                    CustomEntryType tp = (CustomEntryType) stringBibtexEntryTypeEntry.getValue();
                    CustomEntryTypesManager.save(tp, fw);
                    fw.write(Globals.NEWLINE);
                }

            }
        } catch (Throwable ex) {
            session.cancel();
            //repairAfterError(file, backup, status);
            throw new SaveException(ex.getMessage(), ex.getLocalizedMessage(), 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;
        // 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("Cannot find layout file: '" + name + "'.");
            }
        } else {
            File f = new File(name);
            try {
                reader = new FileReader(f);
            } catch (FileNotFoundException ex) {
                throw new IOException("Cannot 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.
     */
    public static List<BibEntry> getSortedEntries(BibDatabase database, MetaData metaData, Set<String> keySet,
            boolean isSaveOperation) {
        boolean inOriginalOrder;
        if (isSaveOperation) {
            Vector<String> storedSaveOrderConfig = metaData
                    .getData(net.sf.jabref.gui.DatabasePropertiesDialog.SAVE_ORDER_CONFIG);
            if (storedSaveOrderConfig == null) {
                inOriginalOrder = Globals.prefs.getBoolean(JabRefPreferences.SAVE_IN_ORIGINAL_ORDER);
            } else {
                SaveOrderConfig saveOrderConfig = new SaveOrderConfig(storedSaveOrderConfig);
                inOriginalOrder = saveOrderConfig.saveInOriginalOrder;
            }
        } else {
            inOriginalOrder = Globals.prefs.getBoolean(JabRefPreferences.EXPORT_IN_ORIGINAL_ORDER);
        }
        List<Comparator<BibEntry>> comparators;
        if (inOriginalOrder) {
            // Sort entries based on their creation order, utilizing the fact
            // that IDs used for entries are increasing, sortable numbers.
            comparators = new ArrayList<>();
            comparators.add(new CrossRefEntryComparator());
            comparators.add(new IdComparator());
        } else {
            comparators = FileActions.getSaveComparators(isSaveOperation, metaData);
        }

        FieldComparatorStack<BibEntry> comparatorStack = new FieldComparatorStack<>(comparators);
        List<BibEntry> sorter = new ArrayList<>();

        if (keySet == null) {
            keySet = database.getKeySet();
        }

        for (String id : keySet) {
            sorter.add(database.getEntryById(id));
        }

        Collections.sort(sorter, comparatorStack);

        return sorter;
    }

    /**
     * @return true iff the entry has a nonzero value in its field.
     */
    private static boolean nonZeroField(BibEntry be, String field) {
        String o = be.getField(field);

        return (o != null) && !"0".equals(o);
    }
}