package org.jabref.gui.externalfiles; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; 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.ButtonGroup; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import org.jabref.Globals; import org.jabref.JabRefExecutorService; import org.jabref.gui.BasePanel; import org.jabref.gui.IconTheme; import org.jabref.gui.JabRefDialog; import org.jabref.gui.externalfiletype.ExternalFileType; import org.jabref.gui.externalfiletype.ExternalFileTypeEntryEditor; import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.externalfiletype.UnknownExternalFileType; import org.jabref.gui.filelist.FileListEntry; import org.jabref.gui.filelist.FileListEntryEditor; import org.jabref.gui.filelist.FileListTableModel; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.undo.UndoableFieldChange; import org.jabref.gui.worker.AbstractWorker; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.FieldName; import org.jabref.model.entry.LinkedFile; import com.jgoodies.forms.builder.ButtonBarBuilder; import com.jgoodies.forms.builder.FormBuilder; import com.jgoodies.forms.layout.FormLayout; /** * This action goes through all selected entries in the BasePanel, and attempts to autoset the * given external file (pdf, ps, ...) based on the same algorithm used for the "Auto" button in * EntryEditor. */ public class SynchronizeFileField extends AbstractWorker { private final BasePanel panel; private final Object[] brokenLinkOptions = {Localization.lang("Ignore"), Localization.lang("Assign new file"), Localization.lang("Remove link"), Localization.lang("Remove all broken links"), Localization.lang("Quit synchronization")}; private List<BibEntry> sel; private SynchronizeFileField.OptionsDialog optDiag; private int entriesChangedCount; private boolean goOn = true; private boolean autoSet = true; private boolean checkExisting = true; public SynchronizeFileField(BasePanel panel) { this.panel = panel; } @Override public void init() { Collection<BibEntry> col = panel.getDatabase().getEntries(); goOn = true; sel = new ArrayList<>(col); // Ask about rules for the operation: if (optDiag == null) { optDiag = new SynchronizeFileField.OptionsDialog(panel.frame(), panel.getBibDatabaseContext()); } optDiag.setLocationRelativeTo(panel.frame()); optDiag.setVisible(true); if (optDiag.canceled()) { goOn = false; return; } autoSet = !optDiag.isAutoSetNone(); checkExisting = optDiag.isCheckLinks(); panel.output(Localization.lang("Synchronizing file links...")); } @Override public void run() { if (!goOn) { panel.output(Localization.lang("This operation requires one or more entries to be selected.")); return; } entriesChangedCount = 0; panel.frame().setProgressBarValue(0); panel.frame().setProgressBarVisible(true); int weightAutoSet = 10; // autoSet takes 10 (?) times longer than checkExisting int progressBarMax = (autoSet ? weightAutoSet * sel.size() : 0) + (checkExisting ? sel.size() : 0); panel.frame().setProgressBarMaximum(progressBarMax); int progress = 0; final NamedCompound ce = new NamedCompound(Localization.lang("Automatically set file links")); Set<BibEntry> changedEntries = new HashSet<>(); // First we try to autoset fields if (autoSet) { List<BibEntry> entries = new ArrayList<>(sel); // Start the automatically setting process: Runnable r = AutoSetLinks.autoSetLinks(entries, ce, changedEntries, null, panel.getBibDatabaseContext(), null, null); JabRefExecutorService.INSTANCE.executeAndWait(r); } progress += sel.size() * weightAutoSet; panel.frame().setProgressBarValue(progress); // The following loop checks all external links that are already set. if (checkExisting) { boolean removeAllBroken = false; mainLoop: for (BibEntry aSel : sel) { panel.frame().setProgressBarValue(progress++); final Optional<String> old = aSel.getField(FieldName.FILE); // Check if a extension is set: if (old.isPresent() && !(old.get().isEmpty())) { FileListTableModel tableModel = new FileListTableModel(); tableModel.setContentDontGuessTypes(old.get()); for (int j = 0; j < tableModel.getRowCount(); j++) { FileListEntry flEntry = tableModel.getEntry(j); LinkedFile field = flEntry.toParsedFileField(); // See if the link looks like an URL: if (field.isOnlineLink()) { continue; // Don't check the remote file. // TODO: should there be an option to check remote links? } // A variable to keep track of whether this link gets deleted: boolean deleted = false; // Get an absolute path representation: Optional<Path> file = field.findIn(panel.getBibDatabaseContext(), Globals.prefs.getFileDirectoryPreferences()); if ((!file.isPresent()) || !Files.exists(file.get())) { int answer; if (removeAllBroken) { answer = 2; // We should delete this link. } else { answer = JOptionPane.showOptionDialog(panel.frame(), Localization.lang("<HTML>Could not find file '%0'<BR>linked from entry '%1'</HTML>", flEntry.getLink(), aSel.getCiteKeyOptional().orElse(Localization.lang("undefined"))), Localization.lang("Broken link"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, brokenLinkOptions, brokenLinkOptions[0] ); } switch (answer) { case 1: // Assign new file. FileListEntryEditor flEditor = new FileListEntryEditor (panel.frame(), flEntry, false, true, panel.getBibDatabaseContext()); flEditor.setVisible(true, true); break; case 2: // Clear field: tableModel.removeEntry(j); deleted = true; // Make sure we don't investigate this link further. j--; // Step back in the iteration, because we removed an entry. break; case 3: // Clear field: tableModel.removeEntry(j); deleted = true; // Make sure we don't investigate this link further. j--; // Step back in the iteration, because we removed an entry. removeAllBroken = true; // Notify for further cases. break; default: // Cancel break mainLoop; } } // Unless we deleted this link, see if its file type is recognized: if (!deleted && flEntry.getType().isPresent() && (flEntry.getType().get() instanceof UnknownExternalFileType)) { String[] options = new String[] { Localization.lang("Define '%0'", flEntry.getType().get().getName()), Localization.lang("Change file type"), Localization.lang("Cancel")}; String defOption = options[0]; int answer = JOptionPane.showOptionDialog(panel.frame(), Localization.lang("One or more file links are of the type '%0', which is undefined. What do you want to do?", flEntry.getType().get().getName()), Localization.lang("Undefined file type"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, defOption ); if (answer == JOptionPane.CANCEL_OPTION) { // User doesn't want to handle this unknown link type. } else if (answer == JOptionPane.YES_OPTION) { // User wants to define the new file type. Show the dialog: ExternalFileType newType = new ExternalFileType(flEntry.getType().get().getName(), "", "", "", "new", IconTheme.JabRefIcon.FILE.getSmallIcon()); ExternalFileTypeEntryEditor editor = new ExternalFileTypeEntryEditor(panel.frame(), newType); editor.setVisible(true); if (editor.okPressed()) { // Get the old list of types, add this one, and update the list in prefs: List<ExternalFileType> fileTypes = new ArrayList<>( ExternalFileTypes.getInstance().getExternalFileTypeSelection()); fileTypes.add(newType); Collections.sort(fileTypes); ExternalFileTypes.getInstance().setExternalFileTypes(fileTypes); panel.getMainTable().repaint(); } } else { // User wants to change the type of this link. // First get a model of all file links for this entry: FileListEntryEditor editor = new FileListEntryEditor (panel.frame(), flEntry, false, true, panel.getBibDatabaseContext()); editor.setVisible(true, false); } } } if (!tableModel.getStringRepresentation().equals(old.orElse(null))) { // The table has been modified. Store the change: String toSet = tableModel.getStringRepresentation(); if (toSet.isEmpty()) { ce.addEdit(new UndoableFieldChange(aSel, FieldName.FILE, old.orElse(null), null)); aSel.clearField(FieldName.FILE); } else { ce.addEdit(new UndoableFieldChange(aSel, FieldName.FILE, old.orElse(null), toSet)); aSel.setField(FieldName.FILE, toSet); } changedEntries.add(aSel); } } } } if (!changedEntries.isEmpty()) { // Add the undo edit: ce.end(); panel.getUndoManager().addEdit(ce); panel.markBaseChanged(); entriesChangedCount = changedEntries.size(); } } @Override public void update() { if (!goOn) { return; } panel.output(Localization.lang("Finished synchronizing file links. Entries changed: %0.", String.valueOf(entriesChangedCount))); panel.frame().setProgressBarVisible(false); if (entriesChangedCount > 0) { panel.markBaseChanged(); } } static class OptionsDialog extends JabRefDialog { private final JButton ok = new JButton(Localization.lang("OK")); private final JButton cancel = new JButton(Localization.lang("Cancel")); private final BibDatabaseContext databaseContext; private final JRadioButton autoSetUnset = new JRadioButton(Localization.lang("Automatically set file links") + ". " + Localization.lang("Do not overwrite existing links."), true); private final JRadioButton autoSetAll = new JRadioButton(Localization.lang("Automatically set file links") + ". " + Localization.lang("Allow overwriting existing links."), false); private final JRadioButton autoSetNone = new JRadioButton(Localization.lang("Do not automatically set"), false); private final JCheckBox checkLinks = new JCheckBox(Localization.lang("Check existing file links"), true); private boolean canceled = true; public OptionsDialog(JFrame parent, BibDatabaseContext databaseContext) { super(parent, Localization.lang("Synchronize file links"), true, OptionsDialog.class); this.databaseContext = databaseContext; ok.addActionListener(e -> { canceled = false; dispose(); }); Action closeAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { dispose(); } }; cancel.addActionListener(closeAction); InputMap im = cancel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap am = cancel.getActionMap(); im.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close"); am.put("close", closeAction); ButtonGroup bg = new ButtonGroup(); bg.add(autoSetUnset); bg.add(autoSetNone); bg.add(autoSetAll); FormLayout layout = new FormLayout("fill:pref", "pref, 2dlu, pref, 2dlu, pref, pref, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref, 2dlu, pref"); FormBuilder builder = FormBuilder.create().layout(layout); JLabel description = new JLabel("<HTML>" + Localization .lang("Attempt to automatically set file links for your entries. Automatically setting works if " + "a file in your file directory<BR>or a subdirectory is named identically to an entry's BibTeX key, plus extension.") + "</HTML>"); builder.addSeparator(Localization.lang("Automatically set file links")).xy(1, 1); builder.add(description).xy(1, 3); builder.add(autoSetUnset).xy(1, 5); builder.add(autoSetAll).xy(1, 6); builder.add(autoSetNone).xy(1, 7); builder.addSeparator(Localization.lang("Check links")).xy(1, 9); description = new JLabel("<HTML>" + Localization .lang("This makes JabRef look up each file link and check if the file exists. If not, you will be given options<BR>to resolve the problem.") + "</HTML>"); builder.add(description).xy(1, 11); builder.add(checkLinks).xy(1, 13); builder.addSeparator("").xy(1, 15); JPanel main = builder.getPanel(); main.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); ButtonBarBuilder bb = new ButtonBarBuilder(); bb.addGlue(); bb.addButton(ok); bb.addButton(cancel); bb.addGlue(); getContentPane().add(main, BorderLayout.CENTER); getContentPane().add(bb.getPanel(), BorderLayout.SOUTH); pack(); } @Override public void setVisible(boolean visible) { if (visible) { canceled = true; } List<String> dirs = databaseContext.getFileDirectories(Globals.prefs.getFileDirectoryPreferences()); if (dirs.isEmpty()) { autoSetNone.setSelected(true); autoSetNone.setEnabled(false); autoSetAll.setEnabled(false); autoSetUnset.setEnabled(false); } else { autoSetNone.setEnabled(true); autoSetAll.setEnabled(true); autoSetUnset.setEnabled(true); } ok.requestFocus(); super.setVisible(visible); } public boolean isAutoSetNone() { return autoSetNone.isSelected(); } public boolean isCheckLinks() { return checkLinks.isSelected(); } public boolean canceled() { return canceled; } } }