package org.jabref.gui.shared; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.io.UnsupportedEncodingException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.sql.SQLException; import java.util.List; import java.util.Optional; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JTextField; import javax.swing.KeyStroke; import org.jabref.Globals; import org.jabref.JabRefException; import org.jabref.JabRefGUI; import org.jabref.gui.BasePanel; import org.jabref.gui.DialogService; import org.jabref.gui.FXDialogService; import org.jabref.gui.JabRefDialog; import org.jabref.gui.JabRefFrame; import org.jabref.gui.exporter.SaveDatabaseAction; import org.jabref.gui.help.HelpAction; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.logic.help.HelpFile; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.FileExtensions; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.DatabaseLocation; import org.jabref.preferences.JabRefPreferences; import org.jabref.shared.DBMSConnection; import org.jabref.shared.DBMSConnectionProperties; import org.jabref.shared.DBMSType; import org.jabref.shared.exception.DatabaseNotSupportedException; import org.jabref.shared.exception.InvalidDBMSConnectionPropertiesException; import org.jabref.shared.prefs.SharedDatabasePreferences; import org.jabref.shared.security.Password; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class ConnectToSharedDatabaseDialog extends JabRefDialog { private static final Log LOGGER = LogFactory.getLog(ConnectToSharedDatabaseDialog.class); private final JabRefFrame frame; private final GridBagLayout gridBagLayout = new GridBagLayout(); private final GridBagConstraints gridBagConstraints = new GridBagConstraints(); private final JPanel connectionPanel = new JPanel(); private final JPanel filePanel = new JPanel(); private final JPanel buttonPanel = new JPanel(); private final JLabel databaseTypeLabel = new JLabel(Localization.lang("Library type") + ":"); private final JLabel hostPortLabel = new JLabel(Localization.lang("Host") + "/" + Localization.lang("Port") + ":"); private final JLabel databaseLabel = new JLabel(Localization.lang("Library") + ":"); private final JLabel userLabel = new JLabel(Localization.lang("User") + ":"); private final JLabel passwordLabel = new JLabel(Localization.lang("Password") + ":"); private final JTextField hostField = new JTextField(12); private final JTextField portField = new JTextField(4); private final JTextField userField = new JTextField(14); private final JTextField databaseField = new JTextField(14); private final JTextField fileLocationField = new JTextField(20); private final JPasswordField passwordField = new JPasswordField(14); private final JComboBox<DBMSType> dbmsTypeDropDown = new JComboBox<>(); private final JButton connectButton = new JButton(Localization.lang("Connect")); private final JButton cancelButton = new JButton(Localization.lang("Cancel")); private final JButton browseButton = new JButton(Localization.lang("Browse")); private final JButton helpButton = new HelpAction(HelpFile.SQL_DATABASE).getHelpButton(); private final JCheckBox rememberPassword = new JCheckBox(Localization.lang("Remember password?")); private final JCheckBox autosaveFile = new JCheckBox(Localization.lang("Automatically save the library to")); private final SharedDatabasePreferences prefs = new SharedDatabasePreferences(); private DBMSConnectionProperties connectionProperties; /** * @param frame the JabRef Frame */ public ConnectToSharedDatabaseDialog(JabRefFrame frame) { super(frame, Localization.lang("Connect to shared database"), ConnectToSharedDatabaseDialog.class); this.frame = frame; initLayout(); updateEnableState(); applyPreferences(); setupActions(); pack(); setLocationRelativeTo(frame); } public void openSharedDatabase() { if (isSharedDatabaseAlreadyPresent()) { JOptionPane.showMessageDialog(ConnectToSharedDatabaseDialog.this, Localization.lang("You are already connected to a database using entered connection details."), Localization.lang("Warning"), JOptionPane.WARNING_MESSAGE); return; } if (autosaveFile.isSelected()) { Path localFilePath = Paths.get(fileLocationField.getText()); if (Files.exists(localFilePath) && !Files.isDirectory(localFilePath)) { int answer = JOptionPane.showConfirmDialog(this, Localization.lang("'%0' exists. Overwrite file?", localFilePath.getFileName().toString()), Localization.lang("Existing file"), JOptionPane.YES_NO_OPTION); if (answer == JOptionPane.NO_OPTION) { fileLocationField.requestFocus(); return; } } } setLoadingConnectButtonText(true); try { BasePanel panel = new SharedDatabaseUIManager(frame).openNewSharedDatabaseTab(connectionProperties); setPreferences(); dispose(); if (!fileLocationField.getText().isEmpty()) { try { new SaveDatabaseAction(panel, Paths.get(fileLocationField.getText())).runCommand(); } catch (Throwable e) { LOGGER.error("Error while saving the database", e); } } return; // setLoadingConnectButtonText(false) should not be reached regularly. } catch (SQLException | InvalidDBMSConnectionPropertiesException exception) { JOptionPane.showMessageDialog(ConnectToSharedDatabaseDialog.this, exception.getMessage(), Localization.lang("Connection error"), JOptionPane.ERROR_MESSAGE); } catch (DatabaseNotSupportedException exception) { new MigrationHelpDialog(this).setVisible(true); } setLoadingConnectButtonText(false); } /** * Defines and sets the different actions up. */ private void setupActions() { Action openAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { try { checkFields(); connectionProperties = new DBMSConnectionProperties(); connectionProperties.setType((DBMSType) dbmsTypeDropDown.getSelectedItem()); connectionProperties.setHost(hostField.getText()); connectionProperties.setPort(Integer.parseInt(portField.getText())); connectionProperties.setDatabase(databaseField.getText()); connectionProperties.setUser(userField.getText()); connectionProperties.setPassword(new String(passwordField.getPassword())); //JPasswordField.getPassword() does not return a String, but a char array. openSharedDatabase(); } catch (JabRefException exception) { JOptionPane.showMessageDialog(ConnectToSharedDatabaseDialog.this, exception.getMessage(), Localization.lang("Warning"), JOptionPane.WARNING_MESSAGE); } } }; connectButton.addActionListener(openAction); cancelButton.addActionListener(e -> dispose()); /** * Set up a listener which updates the default port number once the selection in dbmsTypeDropDown has changed. */ Action dbmsTypeDropDownAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { portField.setText(Integer.toString(((DBMSType) dbmsTypeDropDown.getSelectedItem()).getDefaultPort())); } }; dbmsTypeDropDown.addActionListener(dbmsTypeDropDownAction); // Add enter button action listener connectButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Enter_pressed"); connectButton.getActionMap().put("Enter_pressed", openAction); browseButton.addActionListener(e -> showFileChooser()); autosaveFile.addActionListener(e -> updateEnableState()); } /** * Fetches possibly saved data and configures the control elements respectively. */ private void applyPreferences() { Optional<String> sharedDatabaseType = prefs.getType(); Optional<String> sharedDatabaseHost = prefs.getHost(); Optional<String> sharedDatabasePort = prefs.getPort(); Optional<String> sharedDatabaseName = prefs.getName(); Optional<String> sharedDatabaseUser = prefs.getUser(); Optional<String> sharedDatabasePassword = prefs.getPassword(); boolean sharedDatabaseRememberPassword = prefs.getRememberPassword(); if (sharedDatabaseType.isPresent()) { Optional<DBMSType> dbmsType = DBMSType.fromString(sharedDatabaseType.get()); if (dbmsType.isPresent()) { dbmsTypeDropDown.setSelectedItem(dbmsType.get()); } } if (sharedDatabaseHost.isPresent()) { hostField.setText(sharedDatabaseHost.get()); } if (sharedDatabasePort.isPresent()) { portField.setText(sharedDatabasePort.get()); } else { portField.setText(Integer.toString(((DBMSType) dbmsTypeDropDown.getSelectedItem()).getDefaultPort())); } if (sharedDatabaseName.isPresent()) { databaseField.setText(sharedDatabaseName.get()); } if (sharedDatabaseUser.isPresent()) { userField.setText(sharedDatabaseUser.get()); } if (sharedDatabasePassword.isPresent() && sharedDatabaseUser.isPresent()) { try { passwordField.setText( new Password(sharedDatabasePassword.get().toCharArray(), sharedDatabaseUser.get()).decrypt()); } catch (GeneralSecurityException | UnsupportedEncodingException e) { LOGGER.error("Could not read the password due to decryption problems.", e); } } rememberPassword.setSelected(sharedDatabaseRememberPassword); } /** * Set up the layout and position the control units in their right place. */ private void initLayout() { setResizable(false); Insets defautInsets = new Insets(4, 15, 4, 4); connectionPanel.setBorder( BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Localization.lang("Connection"))); connectionPanel.setLayout(gridBagLayout); Set<DBMSType> availableDBMSTypes = DBMSConnection.getAvailableDBMSTypes(); DefaultComboBoxModel<DBMSType> comboBoxModel = new DefaultComboBoxModel<>( availableDBMSTypes.toArray(new DBMSType[availableDBMSTypes.size()])); dbmsTypeDropDown.setModel(comboBoxModel); gridBagConstraints.insets = defautInsets; gridBagConstraints.fill = GridBagConstraints.BOTH; gridBagLayout.setConstraints(connectionPanel, gridBagConstraints); //1. column gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; connectionPanel.add(databaseTypeLabel, gridBagConstraints); gridBagConstraints.gridy = 1; connectionPanel.add(hostPortLabel, gridBagConstraints); gridBagConstraints.gridy = 2; connectionPanel.add(databaseLabel, gridBagConstraints); gridBagConstraints.gridy = 3; connectionPanel.add(userLabel, gridBagConstraints); gridBagConstraints.gridy = 4; connectionPanel.add(passwordLabel, gridBagConstraints); // 2. column gridBagConstraints.gridwidth = 2; gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 0; connectionPanel.add(dbmsTypeDropDown, gridBagConstraints); gridBagConstraints.gridy = 1; gridBagConstraints.gridwidth = 1; // the hostField is smaller than the others. gridBagConstraints.insets = new Insets(4, 15, 4, 0); connectionPanel.add(hostField, gridBagConstraints); gridBagConstraints.gridy = 2; gridBagConstraints.gridwidth = 2; gridBagConstraints.insets = defautInsets; connectionPanel.add(databaseField, gridBagConstraints); gridBagConstraints.gridy = 3; connectionPanel.add(userField, gridBagConstraints); gridBagConstraints.gridy = 4; connectionPanel.add(passwordField, gridBagConstraints); gridBagConstraints.gridy = 5; connectionPanel.add(rememberPassword, gridBagConstraints); // 3. column gridBagConstraints.gridx = 2; gridBagConstraints.gridy = 1; gridBagConstraints.gridwidth = 1; gridBagConstraints.insets = new Insets(4, 0, 4, 4); connectionPanel.add(portField, gridBagConstraints); // help button gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 6; gridBagConstraints.insets = new Insets(10, 10, 0, 0); JPanel helpPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); helpPanel.add(helpButton); // add panel getContentPane().setLayout(gridBagLayout); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; gridBagConstraints.gridwidth = 1; gridBagConstraints.insets = new Insets(5, 5, 5, 5); gridBagLayout.setConstraints(connectionPanel, gridBagConstraints); getContentPane().add(connectionPanel); // filePanel filePanel.setBorder( BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Localization.lang("File"))); filePanel.setLayout(gridBagLayout); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; gridBagConstraints.gridwidth = 2; filePanel.add(autosaveFile, gridBagConstraints); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 1; gridBagConstraints.gridwidth = 1; filePanel.add(fileLocationField, gridBagConstraints); gridBagConstraints.gridx = 2; filePanel.add(browseButton, gridBagConstraints); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 1; gridBagConstraints.insets = new Insets(5, 5, 5, 5); gridBagLayout.setConstraints(filePanel, gridBagConstraints); getContentPane().add(filePanel); // buttonPanel buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER)); buttonPanel.add(connectButton); buttonPanel.add(cancelButton); buttonPanel.add(helpPanel); gridBagConstraints.gridy = 2; gridBagConstraints.insets = new Insets(5, 5, 5, 5); gridBagLayout.setConstraints(buttonPanel, gridBagConstraints); getContentPane().add(buttonPanel); setModal(true); // Owner window should be disabled while this dialog is opened. } /** * Saves the data from this dialog persistently to facilitate the usage. */ private void setPreferences() { prefs.setType(dbmsTypeDropDown.getSelectedItem().toString()); prefs.setHost(hostField.getText()); prefs.setPort(portField.getText()); prefs.setName(databaseField.getText()); prefs.setUser(userField.getText()); if (rememberPassword.isSelected()) { try { prefs.setPassword(new Password(passwordField.getPassword(), userField.getText()).encrypt()); } catch (GeneralSecurityException | UnsupportedEncodingException e) { LOGGER.error("Could not store the password due to encryption problems.", e); } } else { prefs.clearPassword(); // for the case that the password is already set } prefs.setRememberPassword(rememberPassword.isSelected()); } private boolean isEmptyField(JTextField field) { return field.getText().trim().length() == 0; } /** * Checks every required text field for emptiness. */ private void checkFields() throws JabRefException { if (isEmptyField(hostField)) { hostField.requestFocus(); throw new JabRefException(Localization.lang("Required field \"%0\" is empty.", Localization.lang("Host"))); } if (isEmptyField(portField)) { portField.requestFocus(); throw new JabRefException(Localization.lang("Required field \"%0\" is empty.", Localization.lang("Port"))); } if (isEmptyField(databaseField)) { databaseField.requestFocus(); throw new JabRefException( Localization.lang("Required field \"%0\" is empty.", Localization.lang("Library"))); } if (isEmptyField(userField)) { userField.requestFocus(); throw new JabRefException(Localization.lang("Required field \"%0\" is empty.", Localization.lang("User"))); } if (autosaveFile.isSelected() && isEmptyField(fileLocationField)) { fileLocationField.requestFocus(); throw new JabRefException(Localization.lang("Please enter a valid file path.")); } } /** * Sets the connectButton according to the current connection state. */ private void setLoadingConnectButtonText(boolean isLoading) { connectButton.setEnabled(!isLoading); if (isLoading) { connectButton.setText(Localization.lang("Connecting...")); } else { connectButton.setText(Localization.lang("Connect")); } } /** * Checks whether a database with the given @link {@link DBMSConnectionProperties} is already opened. */ private boolean isSharedDatabaseAlreadyPresent() { List<BasePanel> panels = JabRefGUI.getMainFrame().getBasePanelList(); return panels.parallelStream().anyMatch(panel -> { BibDatabaseContext context = panel.getBibDatabaseContext(); return ((context.getLocation() == DatabaseLocation.SHARED) && this.connectionProperties.equals(context.getDBMSSynchronizer() .getDBProcessor().getDBMSConnectionProperties())); }); } private void showFileChooser() { FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder() .addExtensionFilter(FileExtensions.BIBTEX_DB) .withDefaultExtension(FileExtensions.BIBTEX_DB) .withInitialDirectory(Globals.prefs.get(JabRefPreferences.WORKING_DIRECTORY)).build(); DialogService ds = new FXDialogService(); Optional<Path> path = DefaultTaskExecutor .runInJavaFXThread(() -> ds.showFileOpenDialog(fileDialogConfiguration)); path.ifPresent(p -> fileLocationField.setText(p.toString())); } private void updateEnableState() { fileLocationField.setEnabled(autosaveFile.isSelected()); browseButton.setEnabled(autosaveFile.isSelected()); } }