package org.jabref.gui.externalfiles;
import java.awt.BorderLayout;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.jabref.Globals;
import org.jabref.gui.BasePanel;
import org.jabref.gui.JabRefFrame;
import org.jabref.gui.externalfiletype.ExternalFileType;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.filelist.FileListEntry;
import org.jabref.gui.filelist.FileListTableModel;
import org.jabref.gui.maintable.MainTable;
import org.jabref.gui.undo.NamedCompound;
import org.jabref.gui.undo.UndoableFieldChange;
import org.jabref.gui.undo.UndoableInsertEntry;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.layout.LayoutFormatterPreferences;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.logic.xmp.XMPUtil;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.FieldName;
import org.jabref.model.entry.IdGenerator;
import org.jabref.model.util.FileHelper;
import org.jabref.preferences.JabRefPreferences;
import com.jgoodies.forms.builder.FormBuilder;
import com.jgoodies.forms.layout.FormLayout;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* This class holds the functionality of autolinking to a file that's dropped
* onto an entry.
* <p>
* Options for handling the files are:
* <p>
* 1) Link to the file in its current position (disabled if the file is remote)
* <p>
* 2) Copy the file to ??? directory, rename after bibtex key, and extension
* <p>
* 3) Move the file to ??? directory, rename after bibtex key, and extension
*/
public class DroppedFileHandler {
private static final Log LOGGER = LogFactory.getLog(DroppedFileHandler.class);
private final JabRefFrame frame;
private final BasePanel panel;
private final JRadioButton linkInPlace = new JRadioButton();
private final JRadioButton copyRadioButton = new JRadioButton();
private final JRadioButton moveRadioButton = new JRadioButton();
private final JLabel destDirLabel = new JLabel();
private final JCheckBox renameCheckBox = new JCheckBox();
private final JTextField renameToTextBox = new JTextField(50);
private final JPanel optionsPanel = new JPanel();
public DroppedFileHandler(JabRefFrame frame, BasePanel panel) {
this.frame = frame;
this.panel = panel;
ButtonGroup grp = new ButtonGroup();
grp.add(linkInPlace);
grp.add(copyRadioButton);
grp.add(moveRadioButton);
FormLayout layout = new FormLayout("left:15dlu,pref,pref,pref", "bottom:14pt,pref,pref,pref,pref");
layout.setRowGroups(new int[][] {{1, 2, 3, 4, 5}});
FormBuilder builder = FormBuilder.create().layout(layout);
builder.add(linkInPlace).xyw(1, 1, 4);
builder.add(destDirLabel).xyw(1, 2, 4);
builder.add(copyRadioButton).xyw(2, 3, 3);
builder.add(moveRadioButton).xyw(2, 4, 3);
builder.add(renameCheckBox).xyw(2, 5, 1);
builder.add(renameToTextBox).xyw(4, 5, 1);
optionsPanel.add(builder.getPanel());
}
/**
* Offer copy/move/linking options for a dragged external file. Perform the
* chosen operation, if any.
*
* @param fileName The name of the dragged file.
* @param fileType The FileType associated with the file.
* @param mainTable The MainTable the file was dragged to.
* @param dropRow The row where the file was dropped.
*/
public void handleDroppedfile(String fileName, ExternalFileType fileType, MainTable mainTable, int dropRow) {
BibEntry entry = mainTable.getEntryAt(dropRow);
handleDroppedfile(fileName, fileType, entry);
}
/**
* @param fileName The name of the dragged file.
* @param fileType The FileType associated with the file.
* @param entry The target entry for the drop.
*/
public void handleDroppedfile(String fileName, ExternalFileType fileType, BibEntry entry) {
NamedCompound edits = new NamedCompound(Localization.lang("Drop %0", fileType.getExtension()));
if (tryXmpImport(fileName, fileType, edits)) {
edits.end();
panel.getUndoManager().addEdit(edits);
return;
}
// Show dialog
if (!showLinkMoveCopyRenameDialog(fileName, fileType, entry, panel.getDatabase())) {
return;
}
/*
* Ok, we're ready to go. See first if we need to do a file copy before
* linking:
*/
boolean success = true;
String destFilename;
if (linkInPlace.isSelected()) {
destFilename = FileUtil.shortenFileName(Paths.get(fileName),
panel.getBibDatabaseContext().getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()))
.toString();
} else {
destFilename = renameCheckBox.isSelected() ? renameToTextBox.getText() : Paths.get(fileName).toString();
if (copyRadioButton.isSelected()) {
success = doCopy(fileName, destFilename, edits);
} else if (moveRadioButton.isSelected()) {
success = doMove(fileName, destFilename, edits);
}
}
if (success) {
doLink(entry, fileType, destFilename, false, edits);
panel.markBaseChanged();
panel.updateEntryEditorIfShowing();
}
edits.end();
panel.getUndoManager().addEdit(edits);
}
// Done by MrDlib
public void linkPdfToEntry(String fileName, MainTable entryTable, int dropRow) {
BibEntry entry = entryTable.getEntryAt(dropRow);
linkPdfToEntry(fileName, entry);
}
public void linkPdfToEntry(String fileName, BibEntry entry) {
Optional<ExternalFileType> optFileType = ExternalFileTypes.getInstance().getExternalFileTypeByExt("pdf");
if (!optFileType.isPresent()) {
LOGGER.warn("No file type with extension 'pdf' registered.");
return;
}
ExternalFileType fileType = optFileType.get();
// Show dialog
if (!showLinkMoveCopyRenameDialog(fileName, fileType, entry, panel.getDatabase())) {
return;
}
/*
* Ok, we're ready to go. See first if we need to do a file copy before
* linking:
*/
boolean success = true;
String destFilename;
NamedCompound edits = new NamedCompound(Localization.lang("Drop %0", fileType.getExtension()));
if (linkInPlace.isSelected()) {
destFilename = FileUtil.shortenFileName(Paths.get(fileName),
panel.getBibDatabaseContext().getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()))
.toString();
} else {
destFilename = renameCheckBox.isSelected() ? renameToTextBox.getText() : new File(fileName).getName();
if (copyRadioButton.isSelected()) {
success = doCopy(fileName, destFilename, edits);
} else if (moveRadioButton.isSelected()) {
success = doMove(fileName, destFilename, edits);
}
}
if (success) {
doLink(entry, fileType, destFilename, false, edits);
panel.markBaseChanged();
}
edits.end();
panel.getUndoManager().addEdit(edits);
}
// Done by MrDlib
private boolean tryXmpImport(String fileName, ExternalFileType fileType, NamedCompound edits) {
if (!"pdf".equals(fileType.getExtension())) {
return false;
}
List<BibEntry> xmpEntriesInFile;
try {
xmpEntriesInFile = XMPUtil.readXMP(fileName, Globals.prefs.getXMPPreferences());
} catch (IOException e) {
LOGGER.warn("Problem reading XMP", e);
return false;
}
if ((xmpEntriesInFile == null) || xmpEntriesInFile.isEmpty()) {
return false;
}
JLabel confirmationMessage = new JLabel(Localization.lang("The PDF contains one or several BibTeX-records.")
+ "\n" + Localization.lang("Do you want to import these as new entries into the current library?"));
JPanel entriesPanel = new JPanel();
entriesPanel.setLayout(new BoxLayout(entriesPanel, BoxLayout.Y_AXIS));
xmpEntriesInFile.forEach(entry -> {
JTextArea entryArea = new JTextArea(entry.toString());
entryArea.setEditable(false);
entriesPanel.add(entryArea);
});
JPanel contentPanel = new JPanel(new BorderLayout());
contentPanel.add(confirmationMessage, BorderLayout.NORTH);
contentPanel.add(entriesPanel, BorderLayout.CENTER);
int reply = JOptionPane.showConfirmDialog(frame, contentPanel,
Localization.lang("XMP-metadata found in PDF: %0", fileName), JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (reply == JOptionPane.CANCEL_OPTION) {
return true; // The user canceled thus that we are done.
}
if (reply == JOptionPane.NO_OPTION) {
return false;
}
// reply == JOptionPane.YES_OPTION)
/*
* TODO Extract Import functionality from ImportMenuItem then we could
* do:
*
* ImportMenuItem importer = new ImportMenuItem(frame, (mainTable ==
* null), new PdfXmpImporter());
*
* importer.automatedImport(new String[] { fileName });
*/
boolean isSingle = xmpEntriesInFile.size() == 1;
BibEntry single = isSingle ? xmpEntriesInFile.get(0) : null;
boolean success = true;
String destFilename;
if (linkInPlace.isSelected()) {
destFilename = FileUtil.shortenFileName(Paths.get(fileName),
panel.getBibDatabaseContext().getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences()))
.toString();
} else {
if (renameCheckBox.isSelected() || (single == null)) {
destFilename = fileName;
} else {
destFilename = single.getCiteKey() + "." + fileType.getExtension();
}
if (copyRadioButton.isSelected()) {
success = doCopy(fileName, destFilename, edits);
} else if (moveRadioButton.isSelected()) {
success = doMove(fileName, destFilename, edits);
}
}
if (success) {
for (BibEntry aXmpEntriesInFile : xmpEntriesInFile) {
aXmpEntriesInFile.setId(IdGenerator.next());
edits.addEdit(new UndoableInsertEntry(panel.getDatabase(), aXmpEntriesInFile, panel));
panel.getDatabase().insertEntry(aXmpEntriesInFile);
doLink(aXmpEntriesInFile, fileType, destFilename, true, edits);
}
panel.markBaseChanged();
panel.updateEntryEditorIfShowing();
}
return true;
}
//
// @return true if user pushed "OK", false otherwise
//
private boolean showLinkMoveCopyRenameDialog(String linkFileName, ExternalFileType fileType, BibEntry entry,
BibDatabase database) {
String dialogTitle = Localization.lang("Link to file %0", linkFileName);
Optional<Path> dir = panel.getBibDatabaseContext()
.getFirstExistingFileDir(Globals.prefs.getFileDirectoryPreferences());
if (!dir.isPresent()) {
destDirLabel.setText(Localization.lang("File directory is not set or does not exist!"));
copyRadioButton.setEnabled(false);
moveRadioButton.setEnabled(false);
renameToTextBox.setEnabled(false);
renameCheckBox.setEnabled(false);
linkInPlace.setSelected(true);
} else {
destDirLabel.setText(Localization.lang("File directory is '%0':", dir.get().toString()));
copyRadioButton.setEnabled(true);
moveRadioButton.setEnabled(true);
renameToTextBox.setEnabled(true);
renameCheckBox.setEnabled(true);
}
ChangeListener cl = arg0 -> {
renameCheckBox.setEnabled(!linkInPlace.isSelected());
renameToTextBox.setEnabled(!linkInPlace.isSelected());
};
linkInPlace.setText(Localization.lang("Leave file in its current directory"));
copyRadioButton.setText(Localization.lang("Copy file to file directory"));
moveRadioButton.setText(Localization.lang("Move file to file directory"));
renameCheckBox.setText(Localization.lang("Rename file to").concat(": "));
LayoutFormatterPreferences layoutPrefs = Globals.prefs
.getLayoutFormatterPreferences(Globals.journalAbbreviationLoader);
// Determine which name to suggest:
String targetName = FileUtil.createFileNameFromPattern(database, entry,
Globals.prefs.get(JabRefPreferences.IMPORT_FILENAMEPATTERN), layoutPrefs);
String fileDirPattern = Globals.prefs.get(JabRefPreferences.IMPORT_FILEDIRPATTERN);
String targetDirName = "";
if (!fileDirPattern.isEmpty()) {
targetDirName = FileUtil.createFileNameFromPattern(database, entry, fileDirPattern, layoutPrefs);
}
if (targetDirName.isEmpty()) {
renameToTextBox.setText(targetName.concat(".").concat(fileType.getExtension()));
} else {
renameToTextBox
.setText(targetDirName.concat("/").concat(targetName.concat(".").concat(fileType.getExtension())));
}
linkInPlace.setSelected(frame.prefs().getBoolean(JabRefPreferences.DROPPEDFILEHANDLER_LEAVE));
copyRadioButton.setSelected(frame.prefs().getBoolean(JabRefPreferences.DROPPEDFILEHANDLER_COPY));
moveRadioButton.setSelected(frame.prefs().getBoolean(JabRefPreferences.DROPPEDFILEHANDLER_MOVE));
renameCheckBox.setSelected(frame.prefs().getBoolean(JabRefPreferences.DROPPEDFILEHANDLER_RENAME));
linkInPlace.addChangeListener(cl);
cl.stateChanged(new ChangeEvent(linkInPlace));
try {
Object[] messages = {Localization.lang("How would you like to link to '%0'?", linkFileName), optionsPanel};
int reply = JOptionPane.showConfirmDialog(frame, messages, dialogTitle, JOptionPane.OK_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (reply == JOptionPane.OK_OPTION) {
// store user's choice
frame.prefs().putBoolean(JabRefPreferences.DROPPEDFILEHANDLER_LEAVE, linkInPlace.isSelected());
frame.prefs().putBoolean(JabRefPreferences.DROPPEDFILEHANDLER_COPY, copyRadioButton.isSelected());
frame.prefs().putBoolean(JabRefPreferences.DROPPEDFILEHANDLER_MOVE, moveRadioButton.isSelected());
frame.prefs().putBoolean(JabRefPreferences.DROPPEDFILEHANDLER_RENAME, renameCheckBox.isSelected());
return true;
} else {
return false;
}
} finally {
linkInPlace.removeChangeListener(cl);
}
}
/**
* Make a extension to the file.
*
* @param entry The entry to extension from.
* @param fileType The FileType associated with the file.
* @param filename The path to the file.
* @param edits An NamedCompound action this action is to be added to. If none
* is given, the edit is added to the panel's undoManager.
*/
private void doLink(BibEntry entry, ExternalFileType fileType, String filename, boolean avoidDuplicate,
NamedCompound edits) {
Optional<String> oldValue = entry.getField(FieldName.FILE);
FileListTableModel tm = new FileListTableModel();
oldValue.ifPresent(tm::setContent);
// If avoidDuplicate==true, we should check if this file is already linked:
if (avoidDuplicate) {
// For comparison, find the absolute filename:
List<Path> dirs = panel.getBibDatabaseContext()
.getFileDirectoriesAsPaths(Globals.prefs.getFileDirectoryPreferences());
String absFilename;
if (new File(filename).isAbsolute() || dirs.isEmpty()) {
absFilename = filename;
} else {
Optional<Path> file = FileHelper.expandFilenameAsPath(filename, dirs);
if (file.isPresent()) {
absFilename = file.get().toAbsolutePath().toString();
} else {
absFilename = ""; // This shouldn't happen based on the old code, so maybe one should set it something else?
}
}
LOGGER.debug("absFilename: " + absFilename);
for (int i = 0; i < tm.getRowCount(); i++) {
FileListEntry flEntry = tm.getEntry(i);
// Find the absolute filename for this existing link:
String absName = flEntry.toParsedFileField()
.findIn(dirs)
.map(Path::toAbsolutePath)
.map(Path::toString)
.orElse(null);
LOGGER.debug("absName: " + absName);
// If the filenames are equal, we don't need to link, so we simply return:
if (absFilename.equals(absName)) {
return;
}
}
}
tm.addEntry(tm.getRowCount(), new FileListEntry("", filename, fileType));
String newValue = tm.getStringRepresentation();
UndoableFieldChange edit = new UndoableFieldChange(entry, FieldName.FILE, oldValue.orElse(null), newValue);
entry.setField(FieldName.FILE, newValue);
if (edits == null) {
panel.getUndoManager().addEdit(edit);
} else {
edits.addEdit(edit);
}
}
/**
* Move the given file to the base directory for its file type, and rename
* it to the given filename.
*
* @param fileName The name of the source file.
* @param destFilename The destination filename.
* @param edits TODO we should be able to undo this action
* @return true if the operation succeeded.
*/
private boolean doMove(String fileName, String destFilename, NamedCompound edits) {
Optional<Path> dir = panel.getBibDatabaseContext()
.getFirstExistingFileDir(Globals.prefs.getFileDirectoryPreferences());
if (dir.isPresent()) {
Path destFile = dir.get().resolve(destFilename);
if (Files.exists(destFile)) {
int answer = JOptionPane.showConfirmDialog(frame,
Localization.lang("'%0' exists. Overwrite file?", destFile.toString()),
Localization.lang("Overwrite file?"), JOptionPane.YES_NO_OPTION);
if (answer == JOptionPane.NO_OPTION) {
return false;
}
}
Path fromFile = Paths.get(fileName);
try {
if (!Files.exists(destFile)) {
Files.createDirectories(destFile);
}
} catch (IOException e) {
LOGGER.error("Problem creating target directories", e);
}
if (FileUtil.renameFile(fromFile, destFile, true)) {
return true;
} else {
JOptionPane.showMessageDialog(frame,
Localization.lang("Could not move file '%0'.", destFile.toString())
+ Localization.lang("Please move the file manually and link in place."),
Localization.lang("Move file failed"), JOptionPane.ERROR_MESSAGE);
return false;
}
}
return false;
}
/**
* Copy the given file to the base directory for its file type, and give it
* the given name.
*
* @param fileName The name of the source file.
* @param toFile The destination filename. An existing path-component will be removed.
* @param edits TODO we should be able to undo this!
* @return true if the operation succeeded.
*/
private boolean doCopy(String fileName, String toFile, NamedCompound edits) {
List<String> dirs = panel.getBibDatabaseContext()
.getFileDirectories(Globals.prefs.getFileDirectoryPreferences());
int found = -1;
for (int i = 0; i < dirs.size(); i++) {
if (new File(dirs.get(i)).exists()) {
found = i;
break;
}
}
if (found < 0) {
// OOps, we don't know which directory to put it in, or the given
// dir doesn't exist....
// This should not happen!!
LOGGER.warn("Cannot determine destination directory or destination directory does not exist");
return false;
}
Path destFile = Paths.get(dirs.get(found)).resolve(toFile);
if (destFile.toString().equals(fileName)) {
// File is already in the correct position. Don't override!
return true;
}
if (Files.exists(destFile)) {
int answer = JOptionPane.showConfirmDialog(frame,
Localization.lang("'%0' exists. Overwrite file?", destFile.toString()),
Localization.lang("File exists"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (answer == JOptionPane.NO_OPTION) {
return false;
}
}
try {
//copy does not create directories, therefore we have to create them manually
if (!Files.exists(destFile)) {
Files.createDirectories(destFile);
}
FileUtil.copyFile(Paths.get(fileName), destFile, true);
} catch (IOException e) {
LOGGER.error("Problem copying file", e);
return false;
}
return true;
}
}