package org.jabref.gui.importer; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; 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 javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JProgressBar; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import org.jabref.Globals; import org.jabref.JabRefExecutorService; import org.jabref.gui.BasePanel; import org.jabref.gui.DuplicateResolverDialog; import org.jabref.gui.DuplicateResolverDialog.DuplicateResolverResult; import org.jabref.gui.EntryMarker; import org.jabref.gui.GUIGlobals; import org.jabref.gui.IconTheme; import org.jabref.gui.JabRefDialog; import org.jabref.gui.JabRefFrame; import org.jabref.gui.PreviewPanel; import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.externalfiles.AutoSetLinks; import org.jabref.gui.externalfiles.DownloadExternalFile; import org.jabref.gui.externalfiletype.ExternalFileMenuItem; import org.jabref.gui.filelist.FileListEntry; import org.jabref.gui.filelist.FileListEntryEditor; import org.jabref.gui.filelist.FileListTableModel; import org.jabref.gui.groups.GroupTreeNodeViewModel; import org.jabref.gui.groups.UndoableChangeEntriesOfGroup; import org.jabref.gui.help.HelpAction; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.renderer.GeneralRenderer; import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.undo.UndoableInsertEntry; import org.jabref.gui.undo.UndoableRemoveEntry; import org.jabref.gui.util.comparator.IconComparator; import org.jabref.gui.util.component.CheckBoxMessage; import org.jabref.logic.bibtex.DuplicateCheck; import org.jabref.logic.bibtex.comparator.FieldComparator; import org.jabref.logic.bibtexkeypattern.BibtexKeyPatternUtil; import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.ImportInspector; import org.jabref.logic.importer.OutputPrinter; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.UpdateField; import org.jabref.model.Defaults; import org.jabref.model.FieldChange; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.AuthorList; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.FieldProperty; import org.jabref.model.entry.IdGenerator; import org.jabref.model.entry.InternalBibtexFields; import org.jabref.model.groups.AllEntriesGroup; import org.jabref.model.groups.GroupEntryChanger; import org.jabref.model.groups.GroupTreeNode; import org.jabref.model.metadata.MetaData; import org.jabref.model.strings.StringUtil; import org.jabref.preferences.JabRefPreferences; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.SortedList; import ca.odell.glazedlists.event.ListEvent; import ca.odell.glazedlists.event.ListEventListener; import ca.odell.glazedlists.gui.AbstractTableComparatorChooser; import ca.odell.glazedlists.gui.TableFormat; import ca.odell.glazedlists.swing.DefaultEventSelectionModel; import ca.odell.glazedlists.swing.DefaultEventTableModel; import ca.odell.glazedlists.swing.GlazedListsSwing; import ca.odell.glazedlists.swing.TableComparatorChooser; import com.jgoodies.forms.builder.ButtonBarBuilder; import com.jgoodies.forms.builder.ButtonStackBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Dialog to allow the selection of entries as part of an Import. * <p> * The usual way to use this class is to pass it to an Importer which will do * the following: * <ul> * <li>Register itself as a callback to get notified if the user wants to stop * the import.</li> * <li>Call setVisible(true) to display the dialog</li> * <li>For each entry that has been found call addEntry(...)</li> * <li>Call entryListComplete() after all entries have been fetched</li> * </ul> * <p> * If the importer wants to cancel the import, it should call the dispose() * method. * <p> * If the importer receives the stopFetching-call, it should stop fetching as * soon as possible (it is not really critical, but good style to not contribute * any more results via addEntry, call entryListComplete() or dispose(), after * receiving this call). */ public class ImportInspectionDialog extends JabRefDialog implements ImportInspector, OutputPrinter { private static final Log LOGGER = LogFactory.getLog(ImportInspectionDialog.class); private static final List<String> INSPECTION_FIELDS = Arrays.asList(FieldName.AUTHOR, FieldName.TITLE, FieldName.YEAR, BibEntry.KEY_FIELD); private static final int DUPL_COL = 1; private static final int FILE_COL = 2; private static final int URL_COL = 3; private static final int PAD = 4; private final JabRefFrame frame; private final BibDatabaseContext bibDatabaseContext; private final JSplitPane contentPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); private final JTable glTable; private final TableComparatorChooser<BibEntry> comparatorChooser; private final DefaultEventSelectionModel<BibEntry> selectionModel; private final JProgressBar progressBar = new JProgressBar(SwingConstants.HORIZONTAL); private final JButton ok = new JButton(Localization.lang("OK")); private final JButton generate = new JButton(Localization.lang("Generate now")); private final EventList<BibEntry> entries = new BasicEventList<>(); private final SortedList<BibEntry> sortedList; /** * Duplicate resolving may require deletion of old entries. */ private final List<BibEntry> entriesToDelete = new ArrayList<>(); private final String undoName; private final List<CallBack> callBacks = new ArrayList<>(); private final boolean newDatabase; private final JPopupMenu popup = new JPopupMenu(); private final JButton deselectAllDuplicates = new JButton(Localization.lang("Deselect all duplicates")); private final JButton stop = new JButton(Localization.lang("Stop")); private final PreviewPanel preview; private final Rectangle toRect = new Rectangle(0, 0, 1, 1); private final Map<BibEntry, Set<GroupTreeNode>> groupAdditions = new HashMap<>(); private final JCheckBox autoGenerate = new JCheckBox(Localization.lang("Generate keys"), Globals.prefs.getBoolean(JabRefPreferences.GENERATE_KEYS_AFTER_INSPECTION)); private final JLabel duplLabel = new JLabel(IconTheme.JabRefIcon.DUPLICATE.getSmallIcon()); private final JLabel fileLabel = new JLabel(IconTheme.JabRefIcon.FILE.getSmallIcon()); private final JLabel urlLabel = new JLabel(IconTheme.JabRefIcon.WWW.getSmallIcon()); private BasePanel panel; private boolean generatedKeys; // Set to true after keys have been generated. private boolean defaultSelected = true; /** * Creates a dialog that displays the given list of fields in the table. The * dialog allows another process to add entries dynamically while the dialog * is shown. * * @param frame * @param panel */ public ImportInspectionDialog(JabRefFrame frame, BasePanel panel, String undoName, boolean newDatabase) { super(frame, ImportInspectionDialog.class); this.frame = frame; this.panel = panel; this.bibDatabaseContext = (panel == null) ? null : panel.getBibDatabaseContext(); this.undoName = undoName; this.newDatabase = newDatabase; setIconImages(IconTheme.getLogoSet()); preview = new PreviewPanel(panel, bibDatabaseContext); duplLabel.setToolTipText(Localization.lang("Possible duplicate of existing entry. Click to resolve.")); sortedList = new SortedList<>(entries); DefaultEventTableModel<BibEntry> tableModelGl = (DefaultEventTableModel<BibEntry>) GlazedListsSwing .eventTableModelWithThreadProxyList(sortedList, new EntryTableFormat()); glTable = new EntryTable(tableModelGl); GeneralRenderer renderer = new GeneralRenderer(Color.white); glTable.setDefaultRenderer(JLabel.class, renderer); glTable.setDefaultRenderer(String.class, renderer); glTable.getInputMap().put(Globals.getKeyPrefs().getKey(KeyBinding.DELETE_ENTRY), "delete"); DeleteListener deleteListener = new DeleteListener(); glTable.getActionMap().put("delete", deleteListener); selectionModel = (DefaultEventSelectionModel<BibEntry>) GlazedListsSwing .eventSelectionModelWithThreadProxyList(sortedList); glTable.setSelectionModel(selectionModel); selectionModel.getSelected().addListEventListener(new EntrySelectionListener()); comparatorChooser = TableComparatorChooser.install(glTable, sortedList, AbstractTableComparatorChooser.MULTIPLE_COLUMN_KEYBOARD); setupComparatorChooser(); glTable.addMouseListener(new TableClickListener()); setWidths(); getContentPane().setLayout(new BorderLayout()); progressBar.setIndeterminate(true); JPanel centerPan = new JPanel(); centerPan.setLayout(new BorderLayout()); contentPane.setTopComponent(new JScrollPane(glTable)); contentPane.setBottomComponent(preview); centerPan.add(contentPane, BorderLayout.CENTER); centerPan.add(progressBar, BorderLayout.SOUTH); popup.add(deleteListener); popup.addSeparator(); if (!newDatabase && (bibDatabaseContext != null)) { GroupTreeNode node = bibDatabaseContext.getMetaData().getGroups().get(); JMenu groupsAdd = new JMenu(Localization.lang("Add to group")); groupsAdd.setEnabled(false); // Will get enabled if there are groups that can be added to. insertNodes(groupsAdd, node); popup.add(groupsAdd); } // Add "Attach file" menu choices to right click menu: popup.add(new LinkLocalFile()); popup.add(new DownloadFile()); popup.add(new InternalAutoSetLinks()); popup.add(new AttachUrl()); getContentPane().add(centerPan, BorderLayout.CENTER); ButtonBarBuilder bb = new ButtonBarBuilder(); bb.addGlue(); bb.addButton(ok); bb.addButton(stop); JButton cancel = new JButton(Localization.lang("Cancel")); bb.addButton(cancel); bb.addRelatedGap(); JButton help = new HelpAction(HelpFile.IMPORT_INSPECTION).getHelpButton(); bb.addButton(help); bb.addGlue(); bb.getPanel().setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); ButtonStackBuilder builder = new ButtonStackBuilder(); JButton selectAll = new JButton(Localization.lang("Select all")); builder.addButton(selectAll); JButton deselectAll = new JButton(Localization.lang("Deselect all")); builder.addButton(deselectAll); builder.addButton(deselectAllDuplicates); builder.addRelatedGap(); JButton delete = new JButton(Localization.lang("Delete")); builder.addButton(delete); builder.addRelatedGap(); builder.addFixed(autoGenerate); builder.addButton(generate); builder.getPanel().setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); centerPan.add(builder.getPanel(), BorderLayout.WEST); ok.setEnabled(false); generate.setEnabled(false); ok.addActionListener(new OkListener()); cancel.addActionListener(e -> { signalStopFetching(); dispose(); frame.output(Localization.lang("Import canceled by user")); }); generate.addActionListener(e -> { generate.setEnabled(false); generatedKeys = true; // To prevent the button from getting // enabled again. generateKeys(); // Generate the keys. }); stop.addActionListener(e -> { signalStopFetching(); entryListComplete(); }); selectAll.addActionListener(new SelectionButton(true)); deselectAll.addActionListener(new SelectionButton(false)); deselectAllDuplicates.addActionListener(e -> { for (int i = 0; i < glTable.getRowCount(); i++) { if (glTable.getValueAt(i, DUPL_COL) != null) { glTable.setValueAt(false, i, 0); } } glTable.repaint(); }); deselectAllDuplicates.setEnabled(false); delete.addActionListener(deleteListener); getContentPane().add(bb.getPanel(), BorderLayout.SOUTH); // Remember and default to last size: setSize(new Dimension(Globals.prefs.getInt(JabRefPreferences.IMPORT_INSPECTION_DIALOG_WIDTH), Globals.prefs.getInt(JabRefPreferences.IMPORT_INSPECTION_DIALOG_HEIGHT))); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { contentPane.setDividerLocation(0.5f); } @Override public void windowClosed(WindowEvent e) { Globals.prefs.putInt(JabRefPreferences.IMPORT_INSPECTION_DIALOG_WIDTH, getSize().width); Globals.prefs.putInt(JabRefPreferences.IMPORT_INSPECTION_DIALOG_HEIGHT, getSize().height); } }); // Key bindings: Action closeAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { dispose(); } }; ActionMap am = contentPane.getActionMap(); InputMap im = contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close"); am.put("close", closeAction); } /* (non-Javadoc) * @see package org.jabref.logic.importer.ImportInspector#setProgress(int, int) */ @Override public void setProgress(int current, int max) { SwingUtilities.invokeLater(() -> { progressBar.setIndeterminate(false); progressBar.setMinimum(0); progressBar.setMaximum(max); progressBar.setValue(current); }); } /* (non-Javadoc) * @see package org.jabref.logic.importer.ImportInspector#addEntry(org.jabref.model.entry.BibEntry) */ @Override public void addEntry(BibEntry entry) { List<BibEntry> list = new ArrayList<>(); list.add(entry); addEntries(list); } public void addEntries(Collection<BibEntry> entriesToAdd) { for (BibEntry entry : entriesToAdd) { // We exploit the entry's search status for indicating "Keep" // status: entry.setSearchHit(defaultSelected); // We exploit the entry's group status for indicating duplicate // status. // Checking duplicates means both checking against the background // database (if // applicable) and against entries already in the table. if ((panel != null) && (DuplicateCheck .containsDuplicate(panel.getDatabase(), entry, panel.getBibDatabaseContext().getMode()).isPresent() || (internalDuplicate(this.entries, entry).isPresent()))) { entry.setGroupHit(true); SwingUtilities.invokeLater(() -> deselectAllDuplicates.setEnabled(true)); } this.entries.getReadWriteLock().writeLock().lock(); try { this.entries.add(entry); } finally { this.entries.getReadWriteLock().writeLock().unlock(); } } } /** * Checks if there are duplicates to the given entry in the Collection. Does * not report the entry as duplicate of itself if it is in the Collection. * * @param entriesDupe A Collection of BibEntry instances. * @param entry The entry to search for duplicates of. * @return A possible duplicate, if any, or null if none were found. */ private Optional<BibEntry> internalDuplicate(Collection<BibEntry> entriesDupe, BibEntry entry) { for (BibEntry othEntry : entriesDupe) { if (othEntry.equals(entry)) { continue; // Don't compare the entry to itself } if (DuplicateCheck.isDuplicate(entry, othEntry, panel.getBibDatabaseContext().getMode())) { return Optional.of(othEntry); } } return Optional.empty(); } public void entryListComplete() { SwingUtilities.invokeLater(() -> { progressBar.setIndeterminate(false); progressBar.setVisible(false); ok.setEnabled(true); if (!generatedKeys) { generate.setEnabled(true); } stop.setEnabled(false); //This is for selecting and displaying the first entry in the glTable this.glTable.repaint(); if ((this.glTable.getSelectedRowCount() == 0) && (this.glTable.getRowCount() > 0)) { this.glTable.setRowSelectionInterval(0, 0); //Select first row in the table } }); } /** * Generate key for an entry. */ private void generateKeyForEntry(BibEntry entry) { entries.getReadWriteLock().writeLock().lock(); try { BibDatabase database; MetaData localMetaData; // Relate to existing database, if any: if (panel == null) { database = new BibDatabase(); localMetaData = new MetaData(); } else { database = panel.getDatabase(); localMetaData = panel.getBibDatabaseContext().getMetaData(); } entry.setId(IdGenerator.next()); // Add the entry to the database we are working with: database.insertEntry(entry); // Generate a unique key: BibtexKeyPatternUtil.makeAndSetLabel( localMetaData.getCiteKeyPattern(Globals.prefs.getBibtexKeyPatternPreferences().getKeyPattern()), database, entry, Globals.prefs.getBibtexKeyPatternPreferences()); // Remove the entry from the database again, since we only added it in // order to // make sure the key was unique: database.removeEntry(entry); } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } /** * Generate keys for all entries. All keys will be unique with respect to * one another, and, if they are destined for an existing database, with * respect to existing keys in the database. */ private void generateKeys() { entries.getReadWriteLock().writeLock().lock(); try { BibDatabase database; MetaData localMetaData; // Relate to existing database, if any: if (panel == null) { database = new BibDatabase(); localMetaData = new MetaData(); } else { database = panel.getDatabase(); localMetaData = panel.getBibDatabaseContext().getMetaData(); } List<Optional<String>> keys = new ArrayList<>(entries.size()); // Iterate over the entries, add them to the database we are working // with, // and generate unique keys: for (BibEntry entry : entries) { entry.setId(IdGenerator.next()); database.insertEntry(entry); BibtexKeyPatternUtil.makeAndSetLabel( localMetaData.getCiteKeyPattern(Globals.prefs.getBibtexKeyPatternPreferences().getKeyPattern()), database, entry, Globals.prefs.getBibtexKeyPatternPreferences()); // Add the generated key to our list: -- TODO: Why?? keys.add(entry.getCiteKeyOptional()); } preview.update(); preview.repaint(); // Remove the entries from the database again, since they are not // supposed to // added yet. They only needed to be in it while we generated the keys, // to keep // control over key uniqueness. for (BibEntry entry : entries) { database.removeEntry(entry); } } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } private void insertNodes(JMenu menu, GroupTreeNode node) { final AbstractAction action = getAction(node); if (node.getNumberOfChildren() == 0) { menu.add(action); if (action.isEnabled()) { menu.setEnabled(true); } return; } JMenu submenu; if (node.getGroup() instanceof AllEntriesGroup) { for (GroupTreeNode child : node.getChildren()) { insertNodes(menu, child); } } else { submenu = new JMenu('[' + node.getName() + ']'); // setEnabled(true) is done above/below if at least one menu // entry (item or submenu) is enabled submenu.setEnabled(action.isEnabled()); submenu.add(action); submenu.add(new JPopupMenu.Separator()); for (GroupTreeNode child : node.getChildren()) { insertNodes(submenu, child); } menu.add(submenu); if (submenu.isEnabled()) { menu.setEnabled(true); } } } private AbstractAction getAction(GroupTreeNode node) { AbstractAction action = new AddToGroupAction(node); action.setEnabled(node.getGroup() instanceof GroupEntryChanger); return action; } public void addCallBack(CallBack cb) { callBacks.add(cb); } private void signalStopFetching() { callBacks.forEach(CallBack::stopFetching); } private void setWidths() { TableColumnModel cm = glTable.getColumnModel(); cm.getColumn(0).setPreferredWidth(55); cm.getColumn(0).setMinWidth(55); cm.getColumn(0).setMaxWidth(55); for (int i = 1; i < PAD; i++) { // Lock the width of icon columns. cm.getColumn(i).setPreferredWidth(GUIGlobals.WIDTH_ICON_COL); cm.getColumn(i).setMinWidth(GUIGlobals.WIDTH_ICON_COL); cm.getColumn(i).setMaxWidth(GUIGlobals.WIDTH_ICON_COL); } for (int i = 0; i < INSPECTION_FIELDS.size(); i++) { int width = InternalBibtexFields.getFieldLength(INSPECTION_FIELDS.get(i)); glTable.getColumnModel().getColumn(i + PAD).setPreferredWidth(width); } } private void setupComparatorChooser() { // First column: List<Comparator> comparators = comparatorChooser.getComparatorsForColumn(0); comparators.clear(); comparators = comparatorChooser.getComparatorsForColumn(1); comparators.clear(); // Icon columns: for (int i = 2; i < PAD; i++) { comparators = comparatorChooser.getComparatorsForColumn(i); comparators.clear(); if (i == FILE_COL) { comparators.add(new IconComparator(Collections.singletonList(FieldName.FILE))); } else if (i == URL_COL) { comparators.add(new IconComparator(Collections.singletonList(FieldName.URL))); } } // Remaining columns: for (int i = PAD; i < (PAD + INSPECTION_FIELDS.size()); i++) { comparators = comparatorChooser.getComparatorsForColumn(i); comparators.clear(); comparators.add(new FieldComparator(INSPECTION_FIELDS.get(i - PAD))); } sortedList.getReadWriteLock().writeLock().lock(); try { comparatorChooser.appendComparator(PAD, 0, false); } finally { sortedList.getReadWriteLock().writeLock().unlock(); } } /** * The "defaultSelected" boolean value determines if new entries added are * selected for import or not. This value is true by default. * * @param defaultSelected The desired value. */ public void setDefaultSelected(boolean defaultSelected) { this.defaultSelected = defaultSelected; } @Override public void setStatus(String s) { frame.setStatus(s); } @Override public void showMessage(String message, String title, int msgType) { JOptionPane.showMessageDialog(this, message, title, msgType); } @Override public void showMessage(String message) { JOptionPane.showMessageDialog(this, message); } /** * Displays a dialog which tells the user that an error occurred while fetching entries */ public void showErrorMessage(String fetcherTitle, String localizedException) { showMessage(Localization.lang("Error while fetching from %0", fetcherTitle) + "\n" + Localization.lang("Please try again later and/or check your network connection.") + "\n" + localizedException, Localization.lang("Search %0", fetcherTitle), JOptionPane.ERROR_MESSAGE); } public JabRefFrame getFrame() { return frame; } @FunctionalInterface public interface CallBack { /** * This method is called by the dialog when the user has canceled or * signaled a stop. It is expected that any long-running fetch * operations will stop after this method is called. */ void stopFetching(); } /** * Stores the information about the selected entries being scheduled for * addition to this group. The entries are *not* added to the group at this * time. <p/> Synchronizes on this.entries to prevent conflict with threads * that modify the entry list. */ class AddToGroupAction extends AbstractAction { private final GroupTreeNode node; public AddToGroupAction(GroupTreeNode node) { super(node.getName()); this.node = node; } @Override public void actionPerformed(ActionEvent event) { selectionModel.getSelected().getReadWriteLock().writeLock().lock(); try { for (BibEntry entry : selectionModel.getSelected()) { // We store the groups this entry should be added to in a Set in // the Map: Set<GroupTreeNode> groups = groupAdditions.get(entry); if (groups == null) { // No previous definitions, so we create the Set now: groups = new HashSet<>(); groupAdditions.put(entry, groups); } // Add the group: groups.add(node); } } finally { selectionModel.getSelected().getReadWriteLock().writeLock().unlock(); } } } private class OkListener implements ActionListener { @Override public void actionPerformed(ActionEvent event) { // First check if we are supposed to warn about duplicates. If so, // see if there // are unresolved duplicates, and warn if yes. if (Globals.prefs.getBoolean(JabRefPreferences.WARN_ABOUT_DUPLICATES_IN_INSPECTION)) { for (BibEntry entry : entries) { // Only check entries that are to be imported. Keep status // is indicated // through the search hit status of the entry: if (!entry.isSearchHit()) { continue; } // Check if the entry is a suspected, unresolved, duplicate. // This status // is indicated by the entry's group hit status: if (entry.isGroupHit()) { CheckBoxMessage cbm = new CheckBoxMessage( Localization .lang("There are possible duplicates (marked with an icon) that haven't been resolved. Continue?"), Localization.lang("Disable this confirmation dialog"), false); int answer = JOptionPane.showConfirmDialog(ImportInspectionDialog.this, cbm, Localization.lang("Duplicates found"), JOptionPane.YES_NO_OPTION); if (cbm.isSelected()) { Globals.prefs.putBoolean(JabRefPreferences.WARN_ABOUT_DUPLICATES_IN_INSPECTION, false); } if (answer == JOptionPane.NO_OPTION) { return; } break; } } } // The compund undo action used to contain all changes made by this // dialog. NamedCompound ce = new NamedCompound(undoName); // See if we should remove any old entries for duplicate resolving: if (!entriesToDelete.isEmpty()) { removeEntriesToDelete(ce); } // If "Generate keys" is checked, generate keys unless it's already // been done: if (autoGenerate.isSelected() && !generatedKeys) { generateKeys(); } // Remember the choice until next time: Globals.prefs.putBoolean(JabRefPreferences.GENERATE_KEYS_AFTER_INSPECTION, autoGenerate.isSelected()); final List<BibEntry> selected = getSelectedEntries(); if (!selected.isEmpty()) { addSelectedEntries(ce, selected); } dispose(); SwingUtilities.invokeLater(() -> updateGUI(selected.size())); } private void updateGUI(int entryCount) { if (newDatabase) { frame.addTab(panel, true); } panel.markBaseChanged(); if (entryCount == 0) { frame.output(Localization.lang("No entries imported.")); } else { frame.output(Localization.lang("Number of entries successfully imported") + ": " + entryCount); } } private void removeEntriesToDelete(NamedCompound ce) { for (BibEntry entry : entriesToDelete) { ce.addEdit(new UndoableRemoveEntry(panel.getDatabase(), entry, panel)); panel.getDatabase().removeEntry(entry); } } private void addSelectedEntries(NamedCompound ce, final List<BibEntry> selected) { if (newDatabase) { // Create a new BasePanel for the entries: Defaults defaults = new Defaults(Globals.prefs.getDefaultBibDatabaseMode()); panel = new BasePanel(frame, new BibDatabaseContext(defaults)); } boolean groupingCanceled = false; // Set owner/timestamp if options are enabled: UpdateField.setAutomaticFields(selected, Globals.prefs.getUpdateFieldPreferences()); // Mark entries if we should if (EntryMarker.shouldMarkEntries()) { for (BibEntry entry : selected) { EntryMarker.markEntry(entry, EntryMarker.IMPORT_MARK_LEVEL, false, new NamedCompound("")); } } // Check if we should unmark entries before adding the new ones: if (Globals.prefs.getBoolean(JabRefPreferences.UNMARK_ALL_ENTRIES_BEFORE_IMPORTING)) { for (BibEntry entry : panel.getDatabase().getEntries()) { EntryMarker.unmarkEntry(entry, true, panel.getDatabase(), ce); } } for (BibEntry entry : selected) { // Remove settings to group/search hit status: entry.setSearchHit(false); entry.setGroupHit(false); // If this entry should be added to any groups, do it now: Set<GroupTreeNode> groups = groupAdditions.get(entry); if (!groupingCanceled && (groups != null)) { groupingCanceled = addToGroups(ce, entry, groups); } entry.setId(IdGenerator.next()); ce.addEdit(new UndoableInsertEntry(panel.getDatabase(), entry, panel)); } panel.getDatabase().insertEntries(selected); ce.end(); panel.getUndoManager().addEdit(ce); } private boolean addToGroups(NamedCompound ce, BibEntry entry, Set<GroupTreeNode> groups) { boolean groupingCanceled = false; if (!entry.hasCiteKey()) { // The entry has no key, so it can't be added to the // group. // The best course of action is probably to ask the // user if a key should be generated // immediately. int answer = JOptionPane.showConfirmDialog(ImportInspectionDialog.this, Localization.lang("Cannot add entries to group without generating keys. Generate keys now?"), Localization.lang("Add to group"), JOptionPane.YES_NO_OPTION); if (answer == JOptionPane.YES_OPTION) { generateKeys(); } else { groupingCanceled = true; } } // If the key existed, or exists now, go ahead: if (entry.hasCiteKey()) { for (GroupTreeNode node : groups) { if (node.getGroup() instanceof GroupEntryChanger) { // Add the entry: GroupEntryChanger entryChanger = (GroupEntryChanger)node.getGroup(); List<FieldChange> undo = entryChanger.add(Collections.singletonList(entry)); if (!undo.isEmpty()) { ce.addEdit(UndoableChangeEntriesOfGroup.getUndoableEdit(new GroupTreeNodeViewModel(node), undo)); } } } } return groupingCanceled; } /** * This method returns a List containing all entries that are selected * (checkbox checked). * * @return a List containing the selected entries. */ private List<BibEntry> getSelectedEntries() { List<BibEntry> selected = new ArrayList<>(); for (BibEntry entry : entries) { if (entry.isSearchHit()) { selected.add(entry); } } return selected; } } private class DeleteListener extends AbstractAction { public DeleteListener() { super(Localization.lang("Delete"), IconTheme.JabRefIcon.DELETE_ENTRY.getSmallIcon()); } @Override public void actionPerformed(ActionEvent event) { removeSelectedEntries(); } /** * Removes all selected entries from the table. Synchronizes on this.entries * to prevent conflict with addition of new entries. */ private void removeSelectedEntries() { int row = glTable.getSelectedRow(); List<BibEntry> toRemove = new ArrayList<>(); toRemove.addAll(selectionModel.getSelected()); entries.getReadWriteLock().writeLock().lock(); try { for (BibEntry entry : toRemove) { entries.remove(entry); } } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.clearSelection(); if ((row >= 0) && (!entries.isEmpty())) { row = Math.min(entries.size() - 1, row); glTable.addRowSelectionInterval(row, row); } } } private class SelectionButton implements ActionListener { private final Boolean enable; public SelectionButton(boolean enable) { this.enable = enable; } @Override public void actionPerformed(ActionEvent event) { for (int i = 0; i < glTable.getRowCount(); i++) { glTable.setValueAt(enable, i, 0); } glTable.repaint(); } } private class EntrySelectionListener implements ListEventListener<BibEntry> { @Override public void listChanged(ListEvent<BibEntry> listEvent) { if (listEvent.getSourceList().size() == 1) { preview.setEntry(listEvent.getSourceList().get(0)); contentPane.setDividerLocation(0.5f); SwingUtilities.invokeLater(() -> preview.scrollRectToVisible(toRect)); } } } /** * This class handles clicks on the table that should trigger specific * events, like opening the popup menu. */ private class TableClickListener implements MouseListener { private boolean isIconColumn(int col) { return (col == FILE_COL) || (col == URL_COL); } @Override public void mouseClicked(MouseEvent e) { final int col = glTable.columnAtPoint(e.getPoint()); final int row = glTable.rowAtPoint(e.getPoint()); if (isIconColumn(col)) { BibEntry entry = sortedList.get(row); if (col == FILE_COL) { if (entry.hasField(FieldName.FILE)) { FileListTableModel tableModel = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(tableModel::setContent); if (tableModel.getRowCount() == 0) { return; } FileListEntry fl = tableModel.getEntry(0); (new ExternalFileMenuItem(frame, entry, "", fl.getLink(), null, panel.getBibDatabaseContext(), fl.getType())).actionPerformed(null); } } else { // Must be URL_COL openExternalLink(FieldName.URL, e); } } } @Override public void mouseEntered(MouseEvent e) { // Nothing } @Override public void mouseExited(MouseEvent e) { // Nothing } /** * Show right-click menu. If the click happened in an icon column that * presents its own popup menu, show that. Otherwise, show the ordinary * popup menu. * * @param e The mouse event that triggered the popup. */ private void showPopup(MouseEvent e) { final int col = glTable.columnAtPoint(e.getPoint()); if (col == FILE_COL) { showFileFieldMenu(e); } else { showOrdinaryRightClickMenu(e); } } private void showOrdinaryRightClickMenu(MouseEvent e) { popup.show(glTable, e.getX(), e.getY()); } /** * Show the popup menu for the FILE field. * * @param e The mouse event that triggered the popup. */ private void showFileFieldMenu(MouseEvent e) { final int row = glTable.rowAtPoint(e.getPoint()); BibEntry entry = sortedList.get(row); JPopupMenu menu = new JPopupMenu(); int count = 0; FileListTableModel fileList = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(fileList::setContent); // If there are one or more links, open the first one: for (int i = 0; i < fileList.getRowCount(); i++) { FileListEntry flEntry = fileList.getEntry(i); String description = flEntry.getDescription(); if ((description == null) || (description.trim().isEmpty())) { description = flEntry.getLink(); } menu.add(new ExternalFileMenuItem(panel.frame(), entry, description, flEntry.getLink(), flEntry.getType().get().getIcon(), panel.getBibDatabaseContext(), flEntry.getType())); count++; } if (count == 0) { showOrdinaryRightClickMenu(e); } else { menu.show(glTable, e.getX(), e.getY()); } } /** * Open old-style external links after user clicks icon. * * @param fieldName The name of the BibTeX field this icon is used for. * @param e The MouseEvent that triggered this operation. */ private void openExternalLink(String fieldName, MouseEvent e) { final int row = glTable.rowAtPoint(e.getPoint()); BibEntry entry = sortedList.get(row); entry.getField(fieldName).ifPresent(link -> { try { JabRefDesktop.openExternalViewer(panel.getBibDatabaseContext(), link, fieldName); } catch (IOException ex) { LOGGER.warn("Could not open link", ex); } }); } @Override public void mouseReleased(MouseEvent e) { // Check if the user has right-clicked. If so, open the right-click // menu. if (e.isPopupTrigger()) { showPopup(e); } } @Override public void mousePressed(MouseEvent e) { // Check if the user has right-clicked. If so, open the right-click // menu. if (e.isPopupTrigger()) { showPopup(e); return; } // Check if any other action should be taken: final int col = glTable.columnAtPoint(e.getPoint()); final int row = glTable.rowAtPoint(e.getPoint()); // Is this the duplicate icon column, and is there an icon? if ((col == DUPL_COL) && (glTable.getValueAt(row, col) != null)) { BibEntry first = sortedList.get(row); Optional<BibEntry> other = DuplicateCheck.containsDuplicate(panel.getDatabase(), first, panel.getBibDatabaseContext().getMode()); if (other.isPresent()) { // This will be true if the duplicate is in the existing // database. DuplicateResolverDialog diag = new DuplicateResolverDialog(ImportInspectionDialog.this, other.get(), first, DuplicateResolverDialog.DuplicateResolverType.INSPECTION); diag.setLocationRelativeTo(ImportInspectionDialog.this); diag.setVisible(true); ImportInspectionDialog.this.toFront(); if (diag.getSelected() == DuplicateResolverResult.KEEP_LEFT) { // Remove old entry. Or... add it to a list of entries // to be deleted. We only delete // it after Ok is clicked. entriesToDelete.add(other.get()); // Clear duplicate icon, which is controlled by the // group hit // field of the entry: entries.getReadWriteLock().writeLock().lock(); try { first.setGroupHit(false); } finally { entries.getReadWriteLock().writeLock().unlock(); } } else if (diag.getSelected() == DuplicateResolverResult.KEEP_RIGHT) { // Remove the entry from the import inspection dialog. entries.getReadWriteLock().writeLock().lock(); try { entries.remove(first); } finally { entries.getReadWriteLock().writeLock().unlock(); } } else if (diag.getSelected() == DuplicateResolverResult.KEEP_BOTH) { // Do nothing. entries.getReadWriteLock().writeLock().lock(); try { first.setGroupHit(false); } finally { entries.getReadWriteLock().writeLock().unlock(); } } else if (diag.getSelected() == DuplicateResolverResult.KEEP_MERGE) { // Remove old entry. Or... add it to a list of entries // to be deleted. We only delete // it after Ok is clicked. entriesToDelete.add(other.get()); // Store merged entry for later adding // Clear duplicate icon, which is controlled by the // group hit // field of the entry: entries.getReadWriteLock().writeLock().lock(); try { diag.getMergedEntry().setGroupHit(false); diag.getMergedEntry().setSearchHit(true); entries.add(diag.getMergedEntry()); entries.remove(first); first = new BibEntry(); // Reset first so the next duplicate doesn't trigger } finally { entries.getReadWriteLock().writeLock().unlock(); } } } // Check if the duplicate is of another entry in the import: other = internalDuplicate(entries, first); if (other.isPresent()) { DuplicateResolverDialog diag = new DuplicateResolverDialog(ImportInspectionDialog.this, first, other.get(), DuplicateResolverDialog.DuplicateResolverType.DUPLICATE_SEARCH); diag.setLocationRelativeTo(ImportInspectionDialog.this); diag.setVisible(true); ImportInspectionDialog.this.toFront(); DuplicateResolverResult answer = diag.getSelected(); if (answer == DuplicateResolverResult.KEEP_LEFT) { entries.remove(other.get()); first.setGroupHit(false); } else if (answer == DuplicateResolverResult.KEEP_RIGHT) { entries.remove(first); } else if (answer == DuplicateResolverResult.KEEP_BOTH) { first.setGroupHit(false); } else if (answer == DuplicateResolverResult.KEEP_MERGE) { diag.getMergedEntry().setGroupHit(false); diag.getMergedEntry().setSearchHit(true); entries.add(diag.getMergedEntry()); entries.remove(first); entries.remove(other.get()); } } } } } class AttachUrl extends JMenuItem implements ActionListener { public AttachUrl() { super(Localization.lang("Attach URL")); addActionListener(this); } @Override public void actionPerformed(ActionEvent event) { if (selectionModel.getSelected().size() != 1) { return; } BibEntry entry = selectionModel.getSelected().get(0); String result = JOptionPane.showInputDialog(ImportInspectionDialog.this, Localization.lang("Enter URL"), entry.getField(FieldName.URL).orElse("")); entries.getReadWriteLock().writeLock().lock(); try { if (result != null) { if (result.isEmpty()) { entry.clearField(FieldName.URL); } else { entry.setField(FieldName.URL, result); } } } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } } class DownloadFile extends JMenuItem implements ActionListener, DownloadExternalFile.DownloadCallback { private BibEntry entry; public DownloadFile() { super(Localization.lang("Download file")); addActionListener(this); } @Override public void actionPerformed(ActionEvent actionEvent) { if (selectionModel.getSelected().size() != 1) { return; } entry = selectionModel.getSelected().get(0); if (!entry.getCiteKeyOptional().isPresent()) { int answer = JOptionPane.showConfirmDialog(frame, Localization.lang("This entry has no BibTeX key. Generate key now?"), Localization.lang("Download file"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (answer == JOptionPane.OK_OPTION) { generateKeyForEntry(entry); } } DownloadExternalFile def = new DownloadExternalFile(frame, bibDatabaseContext, entry); try { def.download(this); } catch (IOException ex) { LOGGER.warn("Could not download file", ex); } } @Override public void downloadComplete(FileListEntry file) { ImportInspectionDialog.this.toFront(); // Hack FileListTableModel localModel = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(localModel::setContent); localModel.addEntry(localModel.getRowCount(), file); entries.getReadWriteLock().writeLock().lock(); try { entry.setField(FieldName.FILE, localModel.getStringRepresentation()); } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } } private class InternalAutoSetLinks extends JMenuItem implements ActionListener { public InternalAutoSetLinks() { super(Localization.lang("Automatically set file links")); addActionListener(this); } @Override public void actionPerformed(ActionEvent actionEvent) { if (selectionModel.getSelected().size() != 1) { return; } final BibEntry entry = selectionModel.getSelected().get(0); if (!entry.hasCiteKey()) { int answer = JOptionPane.showConfirmDialog(frame, Localization.lang("This entry has no BibTeX key. Generate key now?"), Localization.lang("Download file"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (answer == JOptionPane.OK_OPTION) { generateKeyForEntry(entry); } else { return; // Can't go on without the bibtex key. } } final FileListTableModel localModel = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(localModel::setContent); // We have a static utility method for searching for all relevant // links: JDialog diag = new JDialog(ImportInspectionDialog.this, true); JabRefExecutorService.INSTANCE .execute(AutoSetLinks.autoSetLinks(entry, localModel, bibDatabaseContext, e -> { if (e.getID() > 0) { entries.getReadWriteLock().writeLock().lock(); try { entry.setField(FieldName.FILE, localModel.getStringRepresentation()); } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } } , diag)); } } private class LinkLocalFile extends JMenuItem implements ActionListener, DownloadExternalFile.DownloadCallback { private BibEntry entry; public LinkLocalFile() { super(Localization.lang("Link local file")); addActionListener(this); } @Override public void actionPerformed(ActionEvent actionEvent) { if (selectionModel.getSelected().size() != 1) { return; } entry = selectionModel.getSelected().get(0); FileListEntry flEntry = new FileListEntry("", ""); FileListEntryEditor editor = new FileListEntryEditor(frame, flEntry, false, true, bibDatabaseContext, true); editor.setVisible(true, true); if (editor.okPressed()) { FileListTableModel localModel = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(localModel::setContent); localModel.addEntry(localModel.getRowCount(), flEntry); entries.getReadWriteLock().writeLock().lock(); try { entry.setField(FieldName.FILE, localModel.getStringRepresentation()); } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } } @Override public void downloadComplete(FileListEntry file) { ImportInspectionDialog.this.toFront(); // Hack FileListTableModel localModel = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(localModel::setContent); localModel.addEntry(localModel.getRowCount(), file); entries.getReadWriteLock().writeLock().lock(); try { entry.setField(FieldName.FILE, localModel.getStringRepresentation()); } finally { entries.getReadWriteLock().writeLock().unlock(); } glTable.repaint(); } } class EntryTable extends JTable { private final GeneralRenderer renderer = new GeneralRenderer(Color.white); public EntryTable(TableModel model) { super(model); getTableHeader().setReorderingAllowed(false); } @Override public TableCellRenderer getCellRenderer(int row, int column) { return column == 0 ? getDefaultRenderer(Boolean.class) : renderer; } @Override public Class<?> getColumnClass(int col) { if (col == 0) { return Boolean.class; } else if (col < PAD) { return JLabel.class; } else { return String.class; } } @Override public boolean isCellEditable(int row, int column) { return column == 0; } @Override public void setValueAt(Object value, int row, int column) { // Only column 0, which is controlled by BibEntry.searchHit, is // editable: entries.getReadWriteLock().writeLock().lock(); try { BibEntry entry = sortedList.get(row); entry.setSearchHit((Boolean) value); } finally { entries.getReadWriteLock().writeLock().unlock(); } } } private class EntryTableFormat implements TableFormat<BibEntry> { @Override public int getColumnCount() { return PAD + INSPECTION_FIELDS.size(); } @Override public String getColumnName(int i) { if (i == 0) { return Localization.lang("Keep"); } if (i >= PAD) { return StringUtil.capitalizeFirst(INSPECTION_FIELDS.get(i - PAD)); } return ""; } @Override public Object getColumnValue(BibEntry entry, int i) { if (i == 0) { return entry.isSearchHit() ? Boolean.TRUE : Boolean.FALSE; } else if (i < PAD) { switch (i) { case DUPL_COL: return entry.isGroupHit() ? duplLabel : null; case FILE_COL: if (entry.hasField(FieldName.FILE)) { FileListTableModel model = new FileListTableModel(); entry.getField(FieldName.FILE).ifPresent(model::setContent); fileLabel.setToolTipText(model.getToolTipHTMLRepresentation()); if ((model.getRowCount() > 0) && model.getEntry(0).getType().isPresent()) { fileLabel.setIcon(model.getEntry(0).getType().get().getIcon()); } return fileLabel; } else { return null; } case URL_COL: if (entry.hasField(FieldName.URL)) { urlLabel.setToolTipText(entry.getField(FieldName.URL).orElse("")); return urlLabel; } else { return null; } default: return null; } } else { String field = INSPECTION_FIELDS.get(i - PAD); if (InternalBibtexFields.getFieldProperties(field).contains(FieldProperty.PERSON_NAMES)) { return entry.getField(field).map(AuthorList::fixAuthorNatbib).orElse(""); } else { return entry.getField(field).orElse(null); } } } } }