package org.jabref.gui.mergeentries;
import java.awt.Font;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
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.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import org.jabref.Globals;
import org.jabref.gui.PreviewPanel;
import org.jabref.gui.util.component.DiffHighlightingTextPane;
import org.jabref.logic.bibtex.BibEntryWriter;
import org.jabref.logic.bibtex.LatexFieldFormatter;
import org.jabref.logic.formatter.casechanger.SentenceCaseFormatter;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.strings.DiffHighlighting;
import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.InternalBibtexFields;
import org.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 MARGIN = "10px";
private static final List<JLabel> HEADING_LABELS = new ArrayList<>(6);
private static final CellConstraints CELL_CONSTRAINTS = new CellConstraints();
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")};
// Headings
private final List<String> columnHeadings = Arrays.asList(Localization.lang("Field"),
Localization.lang("Left entry"),
Localization.lang("Left"),
Localization.lang("None"),
Localization.lang("Right"),
Localization.lang("Right entry"));
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();
/**
* 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) {
columnHeadings.set(1, headingLeft);
columnHeadings.set(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();
fillDiffModes();
// 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);
setupHeadingRows();
mainPanel.add(new JSeparator(), CELL_CONSTRAINTS.xyw(1, 3, 11));
setupEntryTypeRow(mergePanel);
int maxLabelWidth = setupFieldRows(mergePanel);
// Create and add scrollpane
scrollPane = new JScrollPane(mergePanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
updateTextPanes(allFields);
mainPanel.add(scrollPane, CELL_CONSTRAINTS.xyw(1, 4, 11));
mainPanel.add(new JSeparator(), CELL_CONSTRAINTS.xyw(1, 5, 11));
synchronizeColumnWidths(mainLayout, mergeLayout, maxLabelWidth);
// Setup a PreviewPanel and a Bibtex source box for the merged entry
mainPanel.add(boldFontLabel(Localization.lang("Merged entry")), CELL_CONSTRAINTS.xyw(1, 6, 6));
entryPreview = new PreviewPanel(null, mergedEntry, null);
mainPanel.add(entryPreview, CELL_CONSTRAINTS.xyw(1, 8, 6));
mainPanel.add(boldFontLabel(Localization.lang("Merged BibTeX source code")), CELL_CONSTRAINTS.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), CELL_CONSTRAINTS.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);
SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar().setValue(0));
}
private int setupFieldRows(JPanel mergePanel) {
// 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, CELL_CONSTRAINTS.xy(1, (2 * row) - 1, "left, top"));
Optional<String> leftString = leftEntry.getField(field);
Optional<String> rightString = rightEntry.getField(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 = new DiffHighlightingTextPane();
mergePanel.add(tf, CELL_CONSTRAINTS.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, CELL_CONSTRAINTS.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 = new DiffHighlightingTextPane();
mergePanel.add(tf, CELL_CONSTRAINTS.xy(11, (2 * row) - 1, "f, f"));
rightTextPanes.put(field, tf);
}
row++;
}
return maxLabelWidth;
}
private void setupEntryTypeRow(JPanel mergePanel) {
// Start with entry type
mergePanel.add(boldFontLabel(Localization.lang("Entry type")), CELL_CONSTRAINTS.xy(1, 1));
JTextPane leftTypeDisplay = new DiffHighlightingTextPane();
leftTypeDisplay.setText(DiffHighlighting.HTML_START + leftEntry.getType() + DiffHighlighting.HTML_END);
mergePanel.add(leftTypeDisplay, CELL_CONSTRAINTS.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, CELL_CONSTRAINTS.xy(5 + (k * 2), 1));
button.addChangeListener(e -> updateAll());
}
typeRadioButtons.get(0).setSelected(true);
}
JTextPane rightTypeDisplay = new DiffHighlightingTextPane();
rightTypeDisplay.setText(DiffHighlighting.HTML_START + rightEntry.getType() + DiffHighlighting.HTML_END);
mergePanel.add(rightTypeDisplay, CELL_CONSTRAINTS.xy(11, 1));
}
private void setupHeadingRows() {
mainPanel.add(boldFontLabel(Localization.lang("Use")), CELL_CONSTRAINTS.xyw(4, 1, 7, "center, bottom"));
mainPanel.add(diffMode, CELL_CONSTRAINTS.xy(11, 1, "right, bottom"));
// Set headings
for (int i = 0; i < 6; i++) {
HEADING_LABELS.add(boldFontLabel(columnHeadings.get(i)));
mainPanel.add(HEADING_LABELS.get(i), CELL_CONSTRAINTS.xy(1 + (i * 2), 2));
}
}
private void fillDiffModes() {
// 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();
});
}
private void synchronizeColumnWidths(FormLayout mainLayout, FormLayout mergeLayout,
int maxLabelWidth) {
// 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, HEADING_LABELS.get(k).getPreferredSize().width);
}
for (int k = 0; k < 3; k++) {
mergeLayout.setColumnSpec(5 + (k * 2), ColumnSpec.decode(rbAlign[k] + ":" + maxRBWidth + "px"));
}
}
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.getField(field).orElse("");
String rightString = rightEntry.getField(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(DiffHighlighting.HTML_START + leftString + DiffHighlighting.HTML_END);
}
if ((rightString != null) && rightTextPanes.containsKey(field)) {
rightTextPanes.get(field).setText(DiffHighlighting.HTML_START + rightString + DiffHighlighting.HTML_END);
}
}
SwingUtilities.invokeLater(() -> scrollPane.getVerticalScrollBar()
.setValue(Math.min(scrollPane.getVerticalScrollBar().getMaximum(), oldScrollPaneValue)));
}
/**
* @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.getField(field).get()); // Will only happen if field exists
} else if (radioButtons.get(field).get(2).isSelected()) {
mergedEntry.setField(field, rightEntry.getField(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(Globals.prefs.getLatexFieldFormatterPreferences()),
false).write(mergedEntry, writer, databaseType);
} catch (IOException ex) {
LOGGER.error("Error in entry", ex);
}
sourceView.setText(writer.getBuffer().toString());
sourceView.setCaretPosition(0);
}
}