net.sf.jabref.gui.mergeentries.MergeEntries.java Source code

Java tutorial

Introduction

Here is the source code for net.sf.jabref.gui.mergeentries.MergeEntries.java

Source

/*  Copyright (C) 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.gui.mergeentries;

import java.awt.Font;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextArea;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;

import net.sf.jabref.Globals;
import net.sf.jabref.gui.PreviewPanel;
import net.sf.jabref.logic.bibtex.BibEntryWriter;
import net.sf.jabref.logic.bibtex.LatexFieldFormatter;
import net.sf.jabref.logic.bibtex.LatexFieldFormatterPreferences;
import net.sf.jabref.logic.formatter.casechanger.SentenceCaseFormatter;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.logic.util.strings.DiffHighlighting;
import net.sf.jabref.model.database.BibDatabaseMode;
import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.model.entry.InternalBibtexFields;
import net.sf.jabref.preferences.JabRefPreferences;

import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.ColumnSpec;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.forms.layout.RowSpec;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Oscar Gustafsson
 *
 *         Class for dealing with merging entries
 */

public class MergeEntries {

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

    private static final String CONTENT_TYPE = "text/html";

    // Headings
    private static final String[] COLUMN_HEADINGS = { Localization.lang("Field"), Localization.lang("Left entry"),
            Localization.lang("Left"), Localization.lang("None"), Localization.lang("Right"),
            Localization.lang("Right entry") };
    private static final String[] DIFF_MODES = { Localization.lang("Plain text"),
            Localization.lang("Show diff") + " - " + Localization.lang("word"),
            Localization.lang("Show diff") + " - " + Localization.lang("character"),
            Localization.lang("Show symmetric diff") + " - " + Localization.lang("word"),
            Localization.lang("Show symmetric diff") + " - " + Localization.lang("character") };

    private static final String HTML_START = "<html><body>";
    private static final String HTML_END = "</body></html>";
    private static final String BODY_STYLE = "body{font:sans-serif}";
    private static final String ADDITION_STYLE = ".add{color:blue;text-decoration:underline}";
    private static final String REMOVAL_STYLE = ".del{color:red;text-decoration:line-through;}";
    private static final String CHANGE_STYLE = ".change{color:#006400;text-decoration:underline}";

    private final Set<String> identicalFields = new HashSet<>();
    private final Set<String> differentFields = new HashSet<>();
    private final BibEntry mergedEntry = new BibEntry();
    private final BibEntry leftEntry;
    private final BibEntry rightEntry;
    private final BibDatabaseMode databaseType;
    private JScrollPane scrollPane;
    private JTextArea sourceView;
    private PreviewPanel entryPreview;
    private Boolean doneBuilding;
    private Boolean identicalTypes;
    private List<JRadioButton> typeRadioButtons;
    private final Set<String> allFields = new TreeSet<>();
    private final JComboBox<String> diffMode = new JComboBox<>();
    private final Map<String, JTextPane> leftTextPanes = new HashMap<>();
    private final Map<String, JTextPane> rightTextPanes = new HashMap<>();
    private final Map<String, List<JRadioButton>> radioButtons = new HashMap<>();

    private final JPanel mainPanel = new JPanel();

    private static final String MARGIN = "10px";

    /**
     * Constructor taking two entries
     *
     * @param entryLeft Left entry
     * @param entryRight Right entry
     * @param type Bib database mode
     */
    public MergeEntries(BibEntry entryLeft, BibEntry entryRight, BibDatabaseMode type) {
        leftEntry = entryLeft;
        rightEntry = entryRight;
        this.databaseType = type;
        initialize();
    }

    /**
     * Constructor with optional column captions for the two entries
     *
     * @param entryLeft Left entry
     * @param entryRight Right entry
     * @param headingLeft Heading for left entry
     * @param headingRight Heading for right entry
     * @param type Bib database mode
     */
    public MergeEntries(BibEntry entryLeft, BibEntry entryRight, String headingLeft, String headingRight,
            BibDatabaseMode type) {
        COLUMN_HEADINGS[1] = headingLeft;
        COLUMN_HEADINGS[5] = headingRight;
        this.leftEntry = entryLeft;
        this.rightEntry = entryRight;

        this.databaseType = type;

        initialize();
    }

    /**
     * Main function for building the merge entry JPanel
     */
    private void initialize() {
        doneBuilding = false;
        setupFields();

        // Fill diff mode combo box
        for (String diffText : DIFF_MODES) {
            diffMode.addItem(diffText);
        }
        diffMode.setSelectedIndex(Math.min(Globals.prefs.getInt(JabRefPreferences.MERGE_ENTRIES_DIFF_MODE),
                diffMode.getItemCount() - 1));
        diffMode.addActionListener(e -> {
            updateTextPanes(differentFields);
            storePreference();
        });

        // Create main layout
        String colSpecMain = "left:pref, 5px, center:3cm:grow, 5px, center:pref, 3px, center:pref, 3px, center:pref, 5px, center:3cm:grow";
        String colSpecMerge = "left:pref, 5px, fill:3cm:grow, 5px, center:pref, 3px, center:pref, 3px, center:pref, 5px, fill:3cm:grow";
        String rowSpec = "pref, pref, 10px, fill:5cm:grow, 10px, pref, 10px, fill:3cm:grow";
        StringBuilder rowBuilder = new StringBuilder("");
        for (int i = 0; i < allFields.size(); i++) {
            rowBuilder.append("pref, 2dlu, ");
        }
        rowBuilder.append("pref");

        JPanel mergePanel = new JPanel();
        FormLayout mainLayout = new FormLayout(colSpecMain, rowSpec);
        FormLayout mergeLayout = new FormLayout(colSpecMerge, rowBuilder.toString());
        mainPanel.setLayout(mainLayout);
        mergePanel.setLayout(mergeLayout);

        CellConstraints cc = new CellConstraints();

        mainPanel.add(boldFontLabel(Localization.lang("Use")), cc.xyw(4, 1, 7, "center, bottom"));
        mainPanel.add(diffMode, cc.xy(11, 1, "right, bottom"));

        // Set headings
        JLabel[] headingLabels = new JLabel[6];
        for (int i = 0; i < 6; i++) {
            headingLabels[i] = boldFontLabel(COLUMN_HEADINGS[i]);
            mainPanel.add(headingLabels[i], cc.xy(1 + (i * 2), 2));

        }

        mainPanel.add(new JSeparator(), cc.xyw(1, 3, 11));

        // Start with entry type
        mergePanel.add(boldFontLabel(Localization.lang("Entry type")), cc.xy(1, 1));

        JTextPane leftTypeDisplay = getStyledTextPane();
        leftTypeDisplay.setText(HTML_START + leftEntry.getType() + HTML_END);
        mergePanel.add(leftTypeDisplay, cc.xy(3, 1));
        if (leftEntry.getType().equals(rightEntry.getType())) {
            identicalTypes = true;
        } else {
            identicalTypes = false;
            ButtonGroup group = new ButtonGroup();
            typeRadioButtons = new ArrayList<>(2);
            for (int k = 0; k < 3; k += 2) {
                JRadioButton button = new JRadioButton();
                typeRadioButtons.add(button);
                group.add(button);
                mergePanel.add(button, cc.xy(5 + (k * 2), 1));
                button.addChangeListener(e -> updateAll());
            }
            typeRadioButtons.get(0).setSelected(true);
        }
        JTextPane rightTypeDisplay = getStyledTextPane();
        rightTypeDisplay.setText(HTML_START + rightEntry.getType() + HTML_END);
        mergePanel.add(rightTypeDisplay, cc.xy(11, 1));

        // For all fields in joint add a row and possibly radio buttons
        int row = 2;
        int maxLabelWidth = -1;
        for (String field : allFields) {
            JLabel label = boldFontLabel(new SentenceCaseFormatter().format(field));
            mergePanel.add(label, cc.xy(1, (2 * row) - 1, "left, top"));
            Optional<String> leftString = leftEntry.getFieldOptional(field);
            Optional<String> rightString = rightEntry.getFieldOptional(field);
            if (leftString.equals(rightString)) {
                identicalFields.add(field);
            } else {
                differentFields.add(field);
            }

            maxLabelWidth = Math.max(maxLabelWidth, label.getPreferredSize().width);

            // Left text pane
            if (leftString.isPresent()) {
                JTextPane tf = getStyledTextPane();
                mergePanel.add(tf, cc.xy(3, (2 * row) - 1, "f, f"));
                leftTextPanes.put(field, tf);
            }

            // Add radio buttons if the two entries do not have identical fields
            if (identicalFields.contains(field)) {
                mergedEntry.setField(field, leftString.get()); // Will only happen if both entries have the field and the content is identical
            } else {
                ButtonGroup group = new ButtonGroup();
                List<JRadioButton> list = new ArrayList<>(3);
                for (int k = 0; k < 3; k++) {
                    JRadioButton button = new JRadioButton();
                    group.add(button);
                    mergePanel.add(button, cc.xy(5 + (k * 2), (2 * row) - 1));
                    button.addChangeListener(e -> updateAll());
                    list.add(button);
                }
                radioButtons.put(field, list);
                if (leftString.isPresent()) {
                    list.get(0).setSelected(true);
                    if (!rightString.isPresent()) {
                        list.get(2).setEnabled(false);
                    }
                } else {
                    list.get(0).setEnabled(false);
                    list.get(2).setSelected(true);
                }
            }

            // Right text pane
            if (rightString.isPresent()) {
                JTextPane tf = getStyledTextPane();
                mergePanel.add(tf, cc.xy(11, (2 * row) - 1, "f, f"));
                rightTextPanes.put(field, tf);
            }
            row++;
        }

        scrollPane = new JScrollPane(mergePanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setBorder(BorderFactory.createEmptyBorder());
        updateTextPanes(allFields);
        mainPanel.add(scrollPane, cc.xyw(1, 4, 11));
        mainPanel.add(new JSeparator(), cc.xyw(1, 5, 11));

        // Synchronize column widths
        String[] rbAlign = { "right", "center", "left" };
        mainLayout.setColumnSpec(1, ColumnSpec.decode(Integer.toString(maxLabelWidth) + "px"));
        Integer maxRBWidth = -1;
        for (int k = 2; k < 5; k++) {
            maxRBWidth = Math.max(maxRBWidth, headingLabels[k].getPreferredSize().width);
        }
        for (int k = 0; k < 3; k++) {
            mergeLayout.setColumnSpec(5 + (k * 2), ColumnSpec.decode(rbAlign[k] + ":" + maxRBWidth + "px"));
        }

        // Setup a PreviewPanel and a Bibtex source box for the merged entry
        mainPanel.add(boldFontLabel(Localization.lang("Merged entry")), cc.xyw(1, 6, 6));

        entryPreview = new PreviewPanel(null, mergedEntry, null, Globals.prefs.get(JabRefPreferences.PREVIEW_0));
        mainPanel.add(entryPreview, cc.xyw(1, 8, 6));

        mainPanel.add(boldFontLabel(Localization.lang("Merged BibTeX source code")), cc.xyw(8, 6, 4));

        sourceView = new JTextArea();
        sourceView.setLineWrap(true);
        sourceView.setFont(new Font("Monospaced", Font.PLAIN, Globals.prefs.getInt(JabRefPreferences.FONT_SIZE)));
        mainPanel.add(new JScrollPane(sourceView), cc.xyw(8, 8, 4));
        sourceView.setEditable(false);

        // Add some margin around the layout
        mainLayout.appendRow(RowSpec.decode(MARGIN));
        mainLayout.appendColumn(ColumnSpec.decode(MARGIN));
        mainLayout.insertRow(1, RowSpec.decode(MARGIN));
        mainLayout.insertColumn(1, ColumnSpec.decode(MARGIN));

        // Everything done, allow any action to actually update the merged entry
        doneBuilding = true;

        updateAll();

        // Show what we've got
        mainPanel.setVisible(true);
        javax.swing.SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar().setValue(0));
    }

    private JLabel boldFontLabel(String text) {
        JLabel label = new JLabel(text);
        Font font = label.getFont();
        label.setFont(font.deriveFont(font.getStyle() | Font.BOLD));
        return label;
    }

    private void storePreference() {
        Globals.prefs.putInt(JabRefPreferences.MERGE_ENTRIES_DIFF_MODE, diffMode.getSelectedIndex());
    }

    private void setupFields() {
        allFields.addAll(leftEntry.getFieldNames());
        allFields.addAll(rightEntry.getFieldNames());

        // Remove internal fields
        Set<String> toberemoved = new TreeSet<>();
        for (String field : allFields) {
            if (InternalBibtexFields.isInternalField(field)) {
                toberemoved.add(field);
            }
        }
        allFields.removeAll(toberemoved);
    }

    private void updateTextPanes(Collection<String> fields) {
        int oldScrollPaneValue = scrollPane.getVerticalScrollBar().getValue();
        for (String field : fields) {
            String leftString = leftEntry.getFieldOptional(field).orElse("");
            String rightString = rightEntry.getFieldOptional(field).orElse("");
            switch (diffMode.getSelectedIndex()) {
            case 0: // Plain text
                break;
            case 1: // Latexdiff style - word
                rightString = DiffHighlighting.generateDiffHighlighting(leftString, rightString, " ");
                break;
            case 2: // Latexdiff style - character
                rightString = DiffHighlighting.generateDiffHighlighting(leftString, rightString, "");
                break;
            case 3: // Symmetric style - word
                String tmpLeftString = DiffHighlighting.generateSymmetricHighlighting(leftString, rightString, " ");
                rightString = DiffHighlighting.generateSymmetricHighlighting(rightString, leftString, " ");
                leftString = tmpLeftString;
                break;
            case 4: // Symmetric style - character
                tmpLeftString = DiffHighlighting.generateSymmetricHighlighting(leftString, rightString, "");
                rightString = DiffHighlighting.generateSymmetricHighlighting(rightString, leftString, "");
                leftString = tmpLeftString;
                break;
            default: // Shouldn't happen
                break;
            }
            if ((leftString != null) && leftTextPanes.containsKey(field)) {
                leftTextPanes.get(field).setText(HTML_START + leftString + HTML_END);
            }
            if ((rightString != null) && rightTextPanes.containsKey(field)) {
                rightTextPanes.get(field).setText(HTML_START + rightString + HTML_END);
            }
        }
        SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar()
                .setValue(Math.min(scrollPane.getVerticalScrollBar().getMaximum(), oldScrollPaneValue)));
    }

    private JTextPane getStyledTextPane() {
        JTextPane pane = new JTextPane();
        pane.setContentType(CONTENT_TYPE);
        StyleSheet sheet = ((HTMLEditorKit) pane.getEditorKit()).getStyleSheet();
        sheet.addRule(BODY_STYLE);
        sheet.addRule(ADDITION_STYLE);
        sheet.addRule(REMOVAL_STYLE);
        sheet.addRule(CHANGE_STYLE);
        pane.setEditable(false);
        return pane;
    }

    /**
     * @return Merged BibEntry
     */
    public BibEntry getMergeEntry() {
        return mergedEntry;
    }

    /**
     * @return The merge entry JPanel
     */
    public JPanel getMergeEntryPanel() {
        return mainPanel;
    }

    /**
     * Update the merged BibEntry with source and preview panel every time something is changed
     */
    private void updateAll() {
        if (!doneBuilding) {
            // If we are not done adding everything, do not do anything...
            return;
        }
        // Check if the type has changed
        if (!identicalTypes && typeRadioButtons.get(0).isSelected()) {
            mergedEntry.setType(leftEntry.getType());
        } else {
            mergedEntry.setType(rightEntry.getType());
        }

        // Check the potentially different fields
        for (String field : differentFields) {
            if (radioButtons.get(field).get(0).isSelected()) {
                mergedEntry.setField(field, leftEntry.getFieldOptional(field).get()); // Will only happen if field exists
            } else if (radioButtons.get(field).get(2).isSelected()) {
                mergedEntry.setField(field, rightEntry.getFieldOptional(field).get()); // Will only happen if field exists
            } else {
                mergedEntry.clearField(field);
            }
        }

        // Update the PreviewPanel
        entryPreview.setEntry(mergedEntry);

        // Update the BibTeX source view
        StringWriter writer = new StringWriter();
        try {
            new BibEntryWriter(
                    new LatexFieldFormatter(LatexFieldFormatterPreferences.fromPreferences(Globals.prefs)), false)
                            .write(mergedEntry, writer, databaseType);
        } catch (IOException ex) {
            LOGGER.error("Error in entry", ex);
        }
        sourceView.setText(writer.getBuffer().toString());
        sourceView.setCaretPosition(0);
    }
}