package org.jabref.gui.importer.actions;
import java.awt.event.ActionEvent;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.swing.Action;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.jabref.Globals;
import org.jabref.JabRefExecutorService;
import org.jabref.gui.BasePanel;
import org.jabref.gui.DialogService;
import org.jabref.gui.FXDialogService;
import org.jabref.gui.IconTheme;
import org.jabref.gui.JabRefFrame;
import org.jabref.gui.actions.MnemonicAwareAction;
import org.jabref.gui.autosaveandbackup.BackupUIManager;
import org.jabref.gui.importer.ParserResultWarningDialog;
import org.jabref.gui.keyboard.KeyBinding;
import org.jabref.gui.shared.SharedDatabaseUIManager;
import org.jabref.gui.util.DefaultTaskExecutor;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.autosaveandbackup.BackupManager;
import org.jabref.logic.importer.OpenDatabase;
import org.jabref.logic.importer.ParserResult;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.FileExtensions;
import org.jabref.logic.util.io.FileBasedLock;
import org.jabref.migrations.FileLinksUpgradeWarning;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.strings.StringUtil;
import org.jabref.preferences.JabRefPreferences;
import org.jabref.shared.exception.DatabaseNotSupportedException;
import org.jabref.shared.exception.InvalidDBMSConnectionPropertiesException;
import org.jabref.shared.exception.NotASharedDatabaseException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
// The action concerned with opening an existing database.
public class OpenDatabaseAction extends MnemonicAwareAction {
public static final Log LOGGER = LogFactory.getLog(OpenDatabaseAction.class);
// List of actions that may need to be called after opening the file. Such as
// upgrade actions etc. that may depend on the JabRef version that wrote the file:
private static final List<GUIPostOpenAction> POST_OPEN_ACTIONS = new ArrayList<>();
static {
// Add the action for checking for new custom entry types loaded from the BIB file:
POST_OPEN_ACTIONS.add(new CheckForNewEntryTypesAction());
// Add the action for the new external file handling system in version 2.3:
POST_OPEN_ACTIONS.add(new FileLinksUpgradeWarning());
// Add the action for warning about and handling duplicate BibTeX keys:
POST_OPEN_ACTIONS.add(new HandleDuplicateWarnings());
}
private final boolean showDialog;
private final JabRefFrame frame;
public OpenDatabaseAction(JabRefFrame frame, boolean showDialog) {
super(IconTheme.JabRefIcon.OPEN.getIcon());
this.frame = frame;
this.showDialog = showDialog;
putValue(Action.NAME, Localization.menuTitle("Open library"));
putValue(Action.ACCELERATOR_KEY, Globals.getKeyPrefs().getKey(KeyBinding.OPEN_DATABASE));
putValue(Action.SHORT_DESCRIPTION, Localization.lang("Open BibTeX library"));
}
/**
* Go through the list of post open actions, and perform those that need to be performed.
*
* @param panel The BasePanel where the database is shown.
* @param result The result of the BIB file parse operation.
*/
public static void performPostOpenActions(BasePanel panel, ParserResult result) {
for (GUIPostOpenAction action : OpenDatabaseAction.POST_OPEN_ACTIONS) {
if (action.isActionNecessary(result)) {
action.performAction(panel, result);
panel.frame().getTabbedPane().setSelectedComponent(panel);
}
}
}
@Override
public void actionPerformed(ActionEvent e) {
List<Path> filesToOpen = new ArrayList<>();
if (showDialog) {
DialogService ds = new FXDialogService();
FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
.addExtensionFilter(FileExtensions.BIBTEX_DB)
.withDefaultExtension(FileExtensions.BIBTEX_DB)
.withInitialDirectory(Paths.get(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)))
.build();
List<Path> chosenFiles = DefaultTaskExecutor
.runInJavaFXThread(() -> ds.showFileOpenDialogAndGetMultipleFiles(fileDialogConfiguration));
filesToOpen.addAll(chosenFiles);
} else {
LOGGER.info(Action.NAME + " " + e.getActionCommand());
filesToOpen.add(Paths.get(StringUtil.getCorrectFileName(e.getActionCommand(), "bib")));
}
openFiles(filesToOpen, true);
}
/**
* Opens the given file. If null or 404, nothing happens
*
* @param file the file, may be null or not existing
*/
public void openFile(Path file, boolean raisePanel) {
List<Path> filesToOpen = new ArrayList<>();
filesToOpen.add(file);
openFiles(filesToOpen, raisePanel);
}
public void openFilesAsStringList(List<String> fileNamesToOpen, boolean raisePanel) {
List<Path> filesToOpen = fileNamesToOpen.stream().map(Paths::get).collect(Collectors.toList());
openFiles(filesToOpen, raisePanel);
}
/**
* Opens the given files. If one of it is null or 404, nothing happens
*
* @param filesToOpen the filesToOpen, may be null or not existing
*/
public void openFiles(List<Path> filesToOpen, boolean raisePanel) {
BasePanel toRaise = null;
int initialCount = filesToOpen.size();
int removed = 0;
// Check if any of the files are already open:
for (Iterator<Path> iterator = filesToOpen.iterator(); iterator.hasNext();) {
Path file = iterator.next();
for (int i = 0; i < frame.getTabbedPane().getTabCount(); i++) {
BasePanel basePanel = frame.getBasePanelAt(i);
if ((basePanel.getBibDatabaseContext().getDatabaseFile().isPresent())
&& basePanel.getBibDatabaseContext().getDatabaseFile().get().equals(file)) {
iterator.remove();
removed++;
// See if we removed the final one. If so, we must perhaps
// raise the BasePanel in question:
if (removed == initialCount) {
toRaise = basePanel;
}
// no more bps to check, we found a matching one
break;
}
}
}
// Run the actual open in a thread to prevent the program
// locking until the file is loaded.
if (!filesToOpen.isEmpty()) {
final List<Path> theFiles = Collections.unmodifiableList(filesToOpen);
JabRefExecutorService.INSTANCE.execute(() -> {
for (Path theFile : theFiles) {
openTheFile(theFile, raisePanel);
}
});
for (Path theFile : theFiles) {
frame.getFileHistory().newFile(theFile.toString());
}
}
// If no files are remaining to open, this could mean that a file was
// already open. If so, we may have to raise the correct tab:
else if (toRaise != null) {
frame.output(Localization.lang("File '%0' is already open.",
toRaise.getBibDatabaseContext().getDatabaseFile().get().getPath()));
frame.getTabbedPane().setSelectedComponent(toRaise);
}
frame.output(Localization.lang("Files opened") + ": " + (filesToOpen.size()));
}
/**
* @param file the file, may be null or not existing
*/
private void openTheFile(Path file, boolean raisePanel) {
Objects.requireNonNull(file);
if (Files.exists(file)) {
Path fileToLoad = file.toAbsolutePath();
frame.output(Localization.lang("Opening") + ": '" + file + "'");
String fileName = file.getFileName().toString();
Globals.prefs.put(JabRefPreferences.WORKING_DIRECTORY, fileToLoad.getParent().toString());
if (FileBasedLock.hasLockFile(file)) {
Optional<FileTime> modificationTime = FileBasedLock.getLockFileTimeStamp(file);
if ((modificationTime.isPresent()) && ((System.currentTimeMillis()
- modificationTime.get().toMillis()) > FileBasedLock.LOCKFILE_CRITICAL_AGE)) {
// The lock file is fairly old, so we can offer to "steal" the file:
int answer = JOptionPane.showConfirmDialog(null,
"<html>" + Localization.lang("Error opening file") + " '" + fileName + "'. "
+ Localization.lang("File is locked by another JabRef instance.") + "<p>"
+ Localization.lang("Do you want to override the file lock?"),
Localization.lang("File locked"), JOptionPane.YES_NO_OPTION);
if (answer == JOptionPane.YES_OPTION) {
FileBasedLock.deleteLockFile(file);
} else {
return;
}
} else if (!FileBasedLock.waitForFileLock(file)) {
JOptionPane.showMessageDialog(null,
Localization.lang("Error opening file") + " '" + fileName + "'. "
+ Localization.lang("File is locked by another JabRef instance."),
Localization.lang("Error"), JOptionPane.ERROR_MESSAGE);
return;
}
}
if (BackupManager.checkForBackupFile(fileToLoad)) {
BackupUIManager.showRestoreBackupDialog(frame, fileToLoad);
}
ParserResult result;
result = OpenDatabase.loadDatabase(fileToLoad.toString(),
Globals.prefs.getImportFormatPreferences());
if (result.getDatabase().isShared()) {
try {
new SharedDatabaseUIManager(frame).openSharedDatabaseFromParserResult(result);
} catch (SQLException | DatabaseNotSupportedException | InvalidDBMSConnectionPropertiesException |
NotASharedDatabaseException e) {
result.getDatabaseContext().clearDatabaseFile(); // do not open the original file
result.getDatabase().clearSharedDatabaseID();
LOGGER.error("Connection error", e);
JOptionPane.showMessageDialog(frame,
e.getMessage() + "\n\n" + Localization.lang("A local copy will be opened."),
Localization.lang("Connection error"), JOptionPane.WARNING_MESSAGE);
}
}
BasePanel panel = addNewDatabase(result, file, raisePanel);
// After adding the database, go through our list and see if
// any post open actions need to be done. For instance, checking
// if we found new entry types that can be imported, or checking
// if the database contents should be modified due to new features
// in this version of JabRef:
final ParserResult finalReferenceToResult = result;
SwingUtilities.invokeLater(() -> OpenDatabaseAction.performPostOpenActions(panel, finalReferenceToResult));
}
}
private BasePanel addNewDatabase(ParserResult result, final Path file, boolean raisePanel) {
BibDatabase database = result.getDatabase();
if (result.hasWarnings()) {
JabRefExecutorService.INSTANCE
.execute(() -> ParserResultWarningDialog.showParserResultWarningDialog(result, frame));
}
BasePanel basePanel = new BasePanel(frame, result.getDatabaseContext());
// file is set to null inside the EventDispatcherThread
SwingUtilities.invokeLater(() -> frame.addTab(basePanel, raisePanel));
if (Objects.nonNull(file)) {
frame.output(Localization.lang("Opened library") + " '" + file.toString() + "' "
+ Localization.lang("with")
+ " "
+ database.getEntryCount() + " " + Localization.lang("entries") + ".");
}
return basePanel;
}
}