content: /*
   * Universal Password Manager
   * Copyright (C) 2005-2013 Adrian Smith
   *
   * This file is part of Universal Password Manager.
   *
   * Universal Password Manager is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation; either version 2 of the License, or
   * (at your option) any later version.
   *
   * Universal Password Manager is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with Universal Password Manager; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
   */
  package com._17od.upm.gui;
  
  import java.awt.Color;
  import java.awt.Cursor;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
  import java.awt.event.WindowFocusListener;
  import java.io.File;
  import java.io.IOException;
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.Arrays;
  
  import javax.swing.JDialog;
  import javax.swing.JFileChooser;
  import javax.swing.JOptionPane;
  import javax.swing.JPasswordField;
  import javax.swing.Timer;
  
  import org.apache.commons.logging.Log;
  import org.apache.commons.logging.LogFactory;
  
  import com._17od.upm.crypto.CryptoException;
  import com._17od.upm.crypto.InvalidPasswordException;
  import com._17od.upm.database.AccountInformation;
  import com._17od.upm.database.AccountsCSVMarshaller;
  import com._17od.upm.database.ExportException;
  import com._17od.upm.database.ImportException;
  import com._17od.upm.database.PasswordDatabase;
  import com._17od.upm.database.PasswordDatabasePersistence;
  import com._17od.upm.database.ProblemReadingDatabaseFile;
  import com._17od.upm.gui.MainWindow.ChangeDatabaseAction;
  import com._17od.upm.transport.Transport;
  import com._17od.upm.transport.TransportException;
  import com._17od.upm.util.FileChangedCallback;
  import com._17od.upm.util.FileMonitor;
  import com._17od.upm.util.Preferences;
  import com._17od.upm.util.Translator;
  import com._17od.upm.util.Util;
  
  
  public class DatabaseActions {
  
      private static Log LOG = LogFactory.getLog(DatabaseActions.class);
  
      private MainWindow mainWindow;
      private PasswordDatabase database;
      private ArrayList accountNames;
      private boolean localDatabaseDirty = true;
      private PasswordDatabasePersistence dbPers;
      private FileMonitor fileMonitor;
      private boolean databaseNeedsReload = false;
  
      private boolean lockIfInactive;
      private int msToWaitBeforeClosingDB;
  
      private boolean runSetDBDirtyThread = true;
  
  
      public DatabaseActions(MainWindow mainWindow) {
          this.mainWindow = mainWindow;
      }
  
  
      /**
       * This method asks the user for the name of a new database and then creates
       * it. If the file already exists then the user is asked if they'd like to
       * overwrite it.
       * @throws CryptoException
       * @throws IOException
       */
      public void newDatabase() throws IOException, CryptoException {
  
          File newDatabaseFile = getSaveAsFile(Translator.translate("newPasswordDatabase"));
          if (newDatabaseFile == null) {
              return;
          }
  
          final JPasswordField masterPassword = new JPasswordField("");
          boolean passwordsMatch = false;
          do {
  
              //Get a new master password for this database from the user
              JPasswordField confirmedMasterPassword = new JPasswordField("");
              JOptionPane pane = new JOptionPane(new Object[] {Translator.translate("enterMasterPassword"), masterPassword, Translator.translate("confirmation"), confirmedMasterPassword}, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
              JDialog dialog = pane.createDialog(mainWindow, Translator.translate("masterPassword"));
              dialog.addWindowFocusListener(new WindowAdapter() {
                  public void windowGainedFocus(WindowEvent e) {
                      masterPassword.requestFocusInWindow();
                  }
              });
              dialog.show();
  
              if (pane.getValue().equals(new Integer(JOptionPane.OK_OPTION))) {
                  if (!Arrays.equals(masterPassword.getPassword(), confirmedMasterPassword.getPassword())) {
                      JOptionPane.showMessageDialog(mainWindow, Translator.translate("passwordsDontMatch"));
                  } else {
                      passwordsMatch = true;
                  }
              } else {
                  return;
              }
  
          } while (passwordsMatch == false);
  
          if (newDatabaseFile.exists()) {
              newDatabaseFile.delete();
          }
  
          database = new PasswordDatabase(newDatabaseFile);
          dbPers = new PasswordDatabasePersistence(masterPassword.getPassword());
          saveDatabase();
          accountNames = new ArrayList();
          doOpenDatabaseActions();
  
          // If a "Database to Load on Startup" hasn't been set yet then ask the
          // user if they'd like to open this database on startup.
          if (Preferences.get(Preferences.ApplicationOptions.DB_TO_LOAD_ON_STARTUP) == null ||
                  Preferences.get(Preferences.ApplicationOptions.DB_TO_LOAD_ON_STARTUP).equals("")) {
              int option = JOptionPane.showConfirmDialog(mainWindow,
                      Translator.translate("setNewLoadOnStartupDatabase"),
                      Translator.translate("newPasswordDatabase"),
                      JOptionPane.YES_NO_OPTION);
              if (option == JOptionPane.YES_OPTION) {
                  Preferences.set(
                          Preferences.ApplicationOptions.DB_TO_LOAD_ON_STARTUP,
                          newDatabaseFile.getAbsolutePath());
                  Preferences.save();
              }
          }
      }
  
  
      public void changeMasterPassword() throws IOException, ProblemReadingDatabaseFile, CryptoException, PasswordDatabaseException, TransportException {
  
          if (getLatestVersionOfDatabase()) {
              //The first task is to get the current master password
              boolean passwordCorrect = false;
              boolean okClicked = true;
              do {
                  char[] password = askUserForPassword(Translator.translate("enterDatabasePassword"));
                  if (password == null) {
                      okClicked = false;
                  } else {
                      try {
                          dbPers.load(database.getDatabaseFile(), password);
                          passwordCorrect = true;
                      } catch (InvalidPasswordException e) {
                          JOptionPane.showMessageDialog(mainWindow, Translator.translate("incorrectPassword"));
                      }
                  }
              } while (!passwordCorrect && okClicked);
  
              //If the master password was entered correctly then the next step is to get the new master password
              if (passwordCorrect == true) {
  
                      final JPasswordField masterPassword = new JPasswordField("");
                      boolean passwordsMatch = false;
                      Object buttonClicked;
  
                      //Ask the user for the new master password
                      //This loop will continue until the two passwords entered match or until the user hits the cancel button
                      do {
  
  
                          JPasswordField confirmedMasterPassword = new JPasswordField("");
                          JOptionPane pane = new JOptionPane(new Object[] {Translator.translate("enterNewMasterPassword"), masterPassword, Translator.translate("confirmation"), confirmedMasterPassword}, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
                          JDialog dialog = pane.createDialog(mainWindow, Translator.translate("changeMasterPassword"));
                          dialog.addWindowFocusListener(new WindowAdapter() {
                              public void windowGainedFocus(WindowEvent e) {
                                  masterPassword.requestFocusInWindow();
                              }
                          });
                          dialog.show();
  
                          buttonClicked = pane.getValue();
                          if (buttonClicked.equals(new Integer(JOptionPane.OK_OPTION))) {
                              if (!Arrays.equals(masterPassword.getPassword(), confirmedMasterPassword.getPassword())) {
                                  JOptionPane.showMessageDialog(mainWindow, Translator.translate("passwordsDontMatch"));
                              } else {
                                  passwordsMatch = true;
                              }
                          }
  
                      } while (buttonClicked.equals(new Integer(JOptionPane.OK_OPTION)) && !passwordsMatch);
  
                      //If the user clicked OK and the passwords match then change the database password
                      if (buttonClicked.equals(new Integer(JOptionPane.OK_OPTION)) && passwordsMatch) {
                          this.dbPers.getEncryptionService().initCipher(masterPassword.getPassword());
                          saveDatabase();
                      }
  
              }
          }
  
      }
  
  
      public void errorHandler(Exception e) {
          e.printStackTrace();
          String errorMessage = e.getMessage();
          if (errorMessage == null) {
              errorMessage = e.getClass().getName();
          }
          JOptionPane.showMessageDialog(mainWindow, errorMessage, Translator.translate("error"), JOptionPane.ERROR_MESSAGE);
      }
  
      private void doCloseDatabaseActions() {
          mainWindow.getAddAccountButton().setEnabled(false);
          mainWindow.getAddAccountMenuItem().setEnabled(false);
          mainWindow.getSearchField().setEnabled(false);
          mainWindow.getSearchField().setText("");
          mainWindow.getSearchIcon().setEnabled(false);
          mainWindow.getResetSearchButton().setEnabled(false);
          mainWindow.getChangeMasterPasswordMenuItem().setEnabled(false);
          mainWindow.getDatabasePropertiesMenuItem().setEnabled(false);
          mainWindow.getExportMenuItem().setEnabled(false);
          mainWindow.getImportMenuItem().setEnabled(false);
  
          mainWindow.setTitle(MainWindow.getApplicationName());
  
          mainWindow.getStatusBar().setText("");
          databaseNeedsReload = false;
  
          SortedListModel listview = (SortedListModel) mainWindow.getAccountsListview().getModel();
          listview.clear();
  
          mainWindow.getEditAccountButton().setEnabled(false);
          mainWindow.getCopyUsernameButton().setEnabled(false);
          mainWindow.getCopyPasswordButton().setEnabled(false);
          mainWindow.getLaunchURLButton().setEnabled(false);
          mainWindow.getDeleteAccountButton().setEnabled(false);
          mainWindow.getEditAccountMenuItem().setEnabled(false);
          mainWindow.getCopyUsernameMenuItem().setEnabled(false);
          mainWindow.getCopyPasswordMenuItem().setEnabled(false);
          mainWindow.getLaunchURLMenuItem().setEnabled(false);
          mainWindow.getDeleteAccountMenuItem().setEnabled(false);
          mainWindow.getViewAccountMenuItem().setEnabled(false);
      }
  
  
      private void doOpenDatabaseActions() {
          mainWindow.getAddAccountButton().setEnabled(true);
          mainWindow.getAddAccountMenuItem().setEnabled(true);
          mainWindow.getSearchField().setEnabled(true);
          mainWindow.getSearchField().setText("");
          mainWindow.getSearchIcon().setEnabled(true);
          mainWindow.getResetSearchButton().setEnabled(true);
          mainWindow.getChangeMasterPasswordMenuItem().setEnabled(true);
          mainWindow.getDatabasePropertiesMenuItem().setEnabled(true);
          mainWindow.getExportMenuItem().setEnabled(true);
          mainWindow.getImportMenuItem().setEnabled(true);
  
          mainWindow.setTitle(database.getDatabaseFile() + " - " + MainWindow.getApplicationName());
  
          setLocalDatabaseDirty(true);
          databaseNeedsReload = false;
  
          accountNames = getAccountNames();
          populateListview(accountNames);
  
          // Start a thread to listen for changes to the db file
          FileChangedCallback callback = new FileChangedCallback() {
              public void fileChanged(File file) {
                  databaseNeedsReload = true;
                  mainWindow.setFileChangedPanelVisible(true);
              }
          };
          fileMonitor = new FileMonitor(database.getDatabaseFile(), callback);
          Thread thread = new Thread(fileMonitor);
          thread.start();
  
          // If the user asked for the db to close after a period of
          // inactivity then register a listener to capture window focus
          // events.
          configureAutoLock();
  
          // Give the search field focus.
          // I'm using requestFocusInWindow() rather than
          // requestFocus() because the javadocs recommend it.
          mainWindow.getSearchField().requestFocusInWindow();
  
          mainWindow.getDatabaseFileChangedPanel().setVisible(false);
      }
  
      private void configureAutoLock() {
          lockIfInactive = Preferences.get(
                  Preferences.ApplicationOptions.DATABASE_AUTO_LOCK, "false").
                      equals("true");
          msToWaitBeforeClosingDB = Preferences.getInt(
                  Preferences.ApplicationOptions.DATABASE_AUTO_LOCK_TIME, 5)
                      * 60 * 1000;
  
          if (lockIfInactive) {
              LOG.debug("Enabling autoclose when focus lost");
              if (mainWindow.getWindowFocusListeners().length == 0) {
                  mainWindow.addWindowFocusListener(new AutoLockDatabaseListener());
              }
          } else {
              LOG.debug("Disabling autoclose when focus lost");
              for (int i=0; i<mainWindow.getWindowFocusListeners().length; i++) {
                  mainWindow.removeWindowFocusListener(
                          mainWindow.getWindowFocusListeners()[i]);
              }
          }
      }
  
      public ArrayList getAccountNames() {
          ArrayList dbAccounts = database.getAccounts();
          ArrayList accountNames = new ArrayList();
          for (int i=0; i<dbAccounts.size(); i++) {
              AccountInformation ai = (AccountInformation) dbAccounts.get(i);
              String accountName = (String) ai.getAccountName();
              accountNames.add(accountName);
          }
          return accountNames;
      }
  
  
  
      /**
       * Prompt the user to enter a password
       * @return The password entered by the user or null of this hit escape/cancel
       */
      private char[] askUserForPassword(String message) {
          char[] password = null;
  
          final JPasswordField masterPassword = new JPasswordField("");
          JOptionPane pane = new JOptionPane(new Object[] {message, masterPassword }, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
          JDialog dialog = pane.createDialog(mainWindow, Translator.translate("masterPassword"));
          dialog.addWindowFocusListener(new WindowAdapter() {
              public void windowGainedFocus(WindowEvent e) {
                  masterPassword.requestFocusInWindow();
              }
          });
          dialog.show();
  
          if (pane.getValue() != null && pane.getValue().equals(new Integer(JOptionPane.OK_OPTION))) {
              password = masterPassword.getPassword();
          }
  
          return password;
      }
  
  
      public void openDatabase(String databaseFilename) throws IOException, ProblemReadingDatabaseFile, CryptoException {
          openDatabase(databaseFilename, null);
      }
  
  
      public void openDatabase(String databaseFilename, char[] password) throws IOException, ProblemReadingDatabaseFile, CryptoException {
  
          boolean passwordCorrect = false;
          boolean okClicked = true;
          while (!passwordCorrect && okClicked) {
              // If we weren't given a password then ask the user to enter one
              if (password == null) {
                  password = askUserForPassword(Translator.translate("enterDatabasePassword"));
                  if (password == null) {
                      okClicked = false;
                  }
              } else {
                  okClicked = true;
              }
  
              if (okClicked) {
                  try {
                      dbPers = new PasswordDatabasePersistence();
                      database = dbPers.load(new File(databaseFilename), password);
                      passwordCorrect = true;
                  } catch (InvalidPasswordException e) {
                      JOptionPane.showMessageDialog(mainWindow, Translator.translate("incorrectPassword"));
                      password = null;
                  }
              }
          }
  
          if (passwordCorrect) {
              doOpenDatabaseActions();
          }
  
      }
  
  
      public void openDatabase() throws IOException, ProblemReadingDatabaseFile, CryptoException {
          JFileChooser fc = new JFileChooser();
          fc.setDialogTitle(Translator.translate("openDatabase"));
          int returnVal = fc.showOpenDialog(mainWindow);
  
          if (returnVal == JFileChooser.APPROVE_OPTION) {
              File databaseFile = fc.getSelectedFile();
              if (databaseFile.exists()) {
                  openDatabase(databaseFile.getAbsolutePath());
              } else {
                  JOptionPane.showMessageDialog(mainWindow, Translator.translate("fileDoesntExistWithName", databaseFile.getAbsolutePath()), Translator.translate("fileDoesntExist"), JOptionPane.ERROR_MESSAGE);
              }
          }
  
          // Stop any "SetDBDirtyThread"s that are running
          runSetDBDirtyThread = false;
      }
  
  
      public void deleteAccount() throws IOException, CryptoException, TransportException, ProblemReadingDatabaseFile, PasswordDatabaseException {
  
          if (getLatestVersionOfDatabase()) {
              SortedListModel listview = (SortedListModel) mainWindow.getAccountsListview().getModel();
              String selectedAccName = (String) mainWindow.getAccountsListview().getSelectedValue();
  
              int buttonSelected = JOptionPane.showConfirmDialog(mainWindow, Translator.translate("askConfirmDeleteAccount", selectedAccName), Translator.translate("confirmDeleteAccount"), JOptionPane.YES_NO_OPTION);
              if (buttonSelected == JOptionPane.OK_OPTION) {
                  //Remove the account from the listview, accountNames arraylist & the database
                  listview.removeElement(selectedAccName);
                  int i = accountNames.indexOf(selectedAccName);
                  accountNames.remove(i);
                  database.deleteAccount(selectedAccName);
                  saveDatabase();
                  //[1375385] Call the filter method so that the listview is
                  //reinitialised with the remaining matching items
                  filter();
              }
          }
  
      }
  
  
      public void addAccount() throws IOException, CryptoException, TransportException, ProblemReadingDatabaseFile, PasswordDatabaseException {
  
          if (getLatestVersionOfDatabase()) {
  
              //Initialise the AccountDialog
              AccountInformation accInfo = new AccountInformation();
              AccountDialog accDialog = new AccountDialog(accInfo, mainWindow, false, accountNames);
              accDialog.pack();
              accDialog.setLocationRelativeTo(mainWindow);
              accDialog.show();
  
              //If the user press OK then save the new account to the database
              if (accDialog.okClicked()) {
                  database.deleteAccount(accInfo.getAccountName());
                  database.addAccount(accInfo);
                  saveDatabase();
                  accountNames.add(accInfo.getAccountName());
                  //[1375390] Ensure that the listview is properly filtered after an add
                  filter();
              }
  
          }
  
      }
  
  
      public AccountInformation getSelectedAccount() {
          String selectedAccName = (String) mainWindow.getAccountsListview().getSelectedValue();
          return database.getAccount(selectedAccName);
      }
  
  
      private boolean getLatestVersionOfDatabase() throws TransportException, ProblemReadingDatabaseFile, IOException, CryptoException, PasswordDatabaseException {
          boolean latestVersionDownloaded = false;
  
          // Ensure we're working with the latest version of the database
          if (databaseHasRemoteInstance() && localDatabaseDirty) {
              int answer = JOptionPane.showConfirmDialog(mainWindow, Translator.translate("askSyncWithRemoteDB"), Translator.translate("syncDatabase"), JOptionPane.YES_NO_OPTION);
              if (answer == JOptionPane.YES_OPTION) {
                  latestVersionDownloaded = syncWithRemoteDatabase();
              }
          } else {
              latestVersionDownloaded = true;
          }
  
          return latestVersionDownloaded;
      }
  
  
      private boolean databaseHasRemoteInstance() {
          if (database.getDbOptions().getRemoteLocation().equals("")) {
              return false;
          } else {
              return true;
          }
      }
  
  
      public void viewAccount() {
          AccountInformation accInfo = getSelectedAccount();
          AccountDialog accDialog = new AccountDialog(accInfo, mainWindow, true, accountNames);
          accDialog.pack();
          accDialog.setLocationRelativeTo(mainWindow);
          accDialog.show();
      }
  
  
      public void editAccount(String accountName) throws TransportException,
              ProblemReadingDatabaseFile, IOException, CryptoException,
              PasswordDatabaseException, InvalidPasswordException, UPMException {
  
          if (getLatestVersionOfDatabase()) {
              AccountInformation accInfo = database.getAccount(accountName);
              if (accInfo == null) {
                  throw new UPMException(
                          Translator.translate(
                                  "accountDoesntExist", accountName));
              }
  
              AccountDialog accDialog = new AccountDialog(accInfo, mainWindow, false, accountNames);
              accDialog.pack();
              accDialog.setLocationRelativeTo(mainWindow);
              accDialog.show();
  
              //If the ok button was clicked then save the account to the database and update the
              //listview with the new account name (if it's changed)
              if (accDialog.okClicked() && accDialog.getAccountChanged()) {
                  accInfo = accDialog.getAccount();
                  database.deleteAccount(accountName);
                  database.addAccount(accInfo);
                  //If the new account name is different to the old account name then update the
                  //accountNames array and refilter the listview
                  if (!accInfo.getAccountName().equals(accountName)) {
                      // User might change the account name for the Authentication Entry
                      // so this has to be checked
                      if (accountName.equals(database.getDbOptions().getAuthDBEntry())) {
                          database.getDbOptions().setAuthDBEntry(accInfo.getAccountName());
                      }
                      int i = accountNames.indexOf(accountName);
                      accountNames.remove(i);
                      accountNames.add(accInfo.getAccountName());
                      //[1375390] Ensure that the listview is properly filtered after an edit
                      filter();
                  }
                  saveDatabase();
              }
          }
  
      }
  
  
      public void filter() {
          String filterStr = mainWindow.getSearchField().getText().toLowerCase();
  
          ArrayList filteredAccountsList = new ArrayList();
          for (int i=0; i<accountNames.size(); i++) {
              String accountName = (String) accountNames.get(i);
              if (filterStr.equals("") || accountName.toLowerCase().indexOf(filterStr) != -1) {
                  filteredAccountsList.add(accountName);
              }
          }
  
          populateListview(filteredAccountsList);
  
          //If there's only one item in the listview then select it
          if (mainWindow.getAccountsListview().getModel().getSize() == 1) {
              mainWindow.getAccountsListview().setSelectedIndex(0);
          }
      }
  
  
      public void populateListview(ArrayList accountNames) {
          SortedListModel listview = (SortedListModel) mainWindow.getAccountsListview().getModel();
  
          listview.clear();
          mainWindow.getAccountsListview().clearSelection();
  
          for (int i=0; i<accountNames.size(); i++) {
              listview.addElement(accountNames.get(i));
          }
  
          setButtonState();
      }
  
  
      public void setButtonState() {
          if (mainWindow.getAccountsListview().getSelectedValue() == null) {
              mainWindow.getEditAccountButton().setEnabled(false);
              mainWindow.getCopyUsernameButton().setEnabled(false);
              mainWindow.getCopyPasswordButton().setEnabled(false);
              mainWindow.getLaunchURLButton().setEnabled(false);
              mainWindow.getDeleteAccountButton().setEnabled(false);
              mainWindow.getEditAccountMenuItem().setEnabled(false);
              mainWindow.getCopyUsernameMenuItem().setEnabled(false);
              mainWindow.getCopyPasswordMenuItem().setEnabled(false);
              mainWindow.getLaunchURLMenuItem().setEnabled(false);
              mainWindow.getDeleteAccountMenuItem().setEnabled(false);
              mainWindow.getViewAccountMenuItem().setEnabled(false);
          } else {
              mainWindow.getEditAccountButton().setEnabled(true);
              mainWindow.getCopyUsernameButton().setEnabled(true);
              mainWindow.getCopyPasswordButton().setEnabled(true);
              mainWindow.getLaunchURLButton().setEnabled(true);
              mainWindow.getDeleteAccountButton().setEnabled(true);
              mainWindow.getEditAccountMenuItem().setEnabled(true);
              mainWindow.getCopyUsernameMenuItem().setEnabled(true);
              mainWindow.getCopyPasswordMenuItem().setEnabled(true);
              mainWindow.getLaunchURLMenuItem().setEnabled(true);
              mainWindow.getDeleteAccountMenuItem().setEnabled(true);
              mainWindow.getViewAccountMenuItem().setEnabled(true);
          }
      }
  
  
      public void options() {
          OptionsDialog oppDialog = new OptionsDialog(mainWindow);
          oppDialog.pack();
          oppDialog.setLocationRelativeTo(mainWindow);
          oppDialog.show();
  
          configureAutoLock();
  
          if (oppDialog.hasLanguageChanged()) {
              mainWindow.initialiseControlsWithDefaultLanguage();
              if (database != null) {
                  setStatusBarText();
              }
          }
      }
  
  
      public void showAbout() {
          AboutDialog aboutDialog = new AboutDialog(mainWindow);
          aboutDialog.pack();
          aboutDialog.setLocationRelativeTo(mainWindow);
          aboutDialog.show();
      }
  
  
      public void resetSearch() {
          mainWindow.getSearchField().setText("");
      }
  
  
      public void showDatabaseProperties() throws ProblemReadingDatabaseFile, IOException, CryptoException, PasswordDatabaseException {
          try {
              if (getLatestVersionOfDatabase()) {
                  DatabasePropertiesDialog dbPropsDialog = new DatabasePropertiesDialog(mainWindow, getAccountNames(), database);
                  dbPropsDialog.pack();
                  dbPropsDialog.setLocationRelativeTo(mainWindow);
                  dbPropsDialog.show();
                  if (dbPropsDialog.getDatabaseNeedsSaving()) {
                      saveDatabase();
                  }
              }
          } catch (TransportException e) {
              int response = JOptionPane.showConfirmDialog(mainWindow, Translator.translate("problemRetrievingRemoteDB"), Translator.translate("detachDatabase"), JOptionPane.YES_NO_OPTION);
              if (response == JOptionPane.YES_OPTION) {
                  database.getDbOptions().setRemoteLocation("");
                  database.getDbOptions().setAuthDBEntry("");
                  saveDatabase();
              }
          }
      }
  
  
      public void openDatabaseFromURL() throws TransportException, IOException, ProblemReadingDatabaseFile, CryptoException {
  
          // Ask the user for the remote database location
          OpenDatabaseFromURLDialog openDBDialog = new OpenDatabaseFromURLDialog(mainWindow);
          openDBDialog.pack();
          openDBDialog.setLocationRelativeTo(mainWindow);
          openDBDialog.show();
  
          if (openDBDialog.getOkClicked()) {
              // Get the remote database options
              String remoteLocation = openDBDialog.getUrlTextField().getText();
              String username = openDBDialog.getUsernameTextField().getText();
              String password = openDBDialog.getPasswordTextField().getText();
  
              // Ask the user for a location to save the database file to
              File saveDatabaseTo = getSaveAsFile(Translator.translate("saveDatabaseAs"));
  
              if (saveDatabaseTo != null) {
  
                  // Download the database
                  Transport transport = Transport.getTransportForURL(new URL(remoteLocation));
                  File downloadedDatabaseFile = transport.getRemoteFile(remoteLocation, username, password);
  
                  // Delete the file is it already exists
                  if (saveDatabaseTo.exists()) {
                      saveDatabaseTo.delete();
                  }
  
                  // Save the downloaded database file to the new location
                  Util.copyFile(downloadedDatabaseFile, saveDatabaseTo);
  
                  // Now open the downloaded database
                  openDatabase(saveDatabaseTo.getAbsolutePath());
  
              }
          }
  
      }
  
      public void reloadDatabase()
              throws InvalidPasswordException, ProblemReadingDatabaseFile, IOException {
          PasswordDatabase reloadedDb = null;
          try {
              reloadedDb = dbPers.load(database.getDatabaseFile());
          } catch (InvalidPasswordException e) {
              // The password for the reloaded database is different to that of
              // the open database
              boolean okClicked = false;
              do {
                  char[] password = askUserForPassword(Translator.translate("enterDatabasePassword"));
                  if (password == null) {
                      okClicked = false;
                  } else {
                      okClicked = true;
                      try {
                          reloadedDb = dbPers.load(database.getDatabaseFile(), password);
                      } catch (InvalidPasswordException invalidPassword) {
                          JOptionPane.showMessageDialog(mainWindow, Translator.translate("incorrectPassword"));
                      } catch (CryptoException e1) {
                          errorHandler(e);
                      }
                  }
              } while (okClicked && reloadedDb == null);
          }
  
          if (reloadedDb != null) {
              database = reloadedDb;
              doOpenDatabaseActions();
          }
      }
  
      public void reloadDatabaseBefore(ChangeDatabaseAction editAction)
              throws InvalidPasswordException, ProblemReadingDatabaseFile,
              IOException {
          boolean proceedWithAction = false;
          if (this.databaseNeedsReload) {
              int answer = JOptionPane.showConfirmDialog(mainWindow,
                      Translator.translate("askReloadDatabase"),
                      Translator.translate("reloadDatabase"),
                      JOptionPane.YES_NO_OPTION);
              if (answer == JOptionPane.YES_OPTION) {
                  proceedWithAction = reloadDatabaseFromDisk();
              }
          } else {
              proceedWithAction = true;
          }
  
          if (proceedWithAction) {
              editAction.doAction();
          }
      }
  
      public boolean reloadDatabaseFromDisk() throws InvalidPasswordException,
              ProblemReadingDatabaseFile, IOException {
          boolean reloadSuccessful = false;
  
          PasswordDatabase reloadedDb = null;
          try {
              reloadedDb = dbPers.load(database.getDatabaseFile());
          } catch (InvalidPasswordException e) {
              // The password for the reloaded database is different to that of
              // the open database
              boolean okClicked = false;
              do {
                  char[] password = askUserForPassword(Translator
                          .translate("enterDatabasePassword"));
                  if (password == null) {
                      okClicked = false;
                  } else {
                      okClicked = true;
                      try {
                          reloadedDb = dbPers.load(database.getDatabaseFile(),
                                  password);
                      } catch (InvalidPasswordException invalidPassword) {
                          JOptionPane.showMessageDialog(mainWindow,
                                  Translator.translate("incorrectPassword"));
                      } catch (CryptoException e1) {
                          errorHandler(e);
                      }
                  }
              } while (okClicked && reloadedDb == null);
          }
  
          if (reloadedDb != null) {
              database = reloadedDb;
              doOpenDatabaseActions();
              reloadSuccessful = true;
          }
  
          return reloadSuccessful;
      }
  
      public boolean syncWithRemoteDatabase() throws TransportException, ProblemReadingDatabaseFile, IOException, CryptoException, PasswordDatabaseException {
  
          boolean syncSuccessful = false;
  
          try {
              fileMonitor.pause();
  
              mainWindow.getContentPane().setCursor(new Cursor(Cursor.WAIT_CURSOR));
  
              // Get the remote database options
              String remoteLocation = database.getDbOptions().getRemoteLocation();
              String authDBEntry = database.getDbOptions().getAuthDBEntry();
              String httpUsername = null;
              String httpPassword = null;
              if (!authDBEntry.equals("")) {
                  httpUsername = database.getAccount(authDBEntry).getUserId();
                  httpPassword = database.getAccount(authDBEntry).getPassword();
              }
  
              // Download the database that's already at the remote location
              Transport transport = Transport.getTransportForURL(new URL(remoteLocation));
              File remoteDatabaseFile = transport.getRemoteFile(remoteLocation, database.getDatabaseFile().getName(), httpUsername, httpPassword);
  
              // Attempt to decrypt the database using the password the user entered
              PasswordDatabase remoteDatabase = null;
              char[] password = null;
              boolean successfullyDecryptedDb = false;
              try {
                  remoteDatabase = dbPers.load(remoteDatabaseFile);
                  successfullyDecryptedDb = true;
              } catch (InvalidPasswordException e) {
                  // The password for the downloaded database is different to that of the open database
                  // (most likely the user changed the local database's master password)
                  boolean okClicked = false;
                  do {
                      password = askUserForPassword(Translator.translate("enterPaswordForRemoteDB"));
                      if (password == null) {
                          okClicked = false;
                      } else {
                          okClicked = true;
                          try {
                              remoteDatabase = dbPers.load(remoteDatabaseFile, password);
                              successfullyDecryptedDb = true;
                          } catch (InvalidPasswordException invalidPassword) {
                              JOptionPane.showMessageDialog(mainWindow, Translator.translate("incorrectPassword"));
                          }
                      }
                  } while (okClicked && !successfullyDecryptedDb);
              }
  
              /* If the local database revision > remote database version => upload local database
                 If the local database revision < remote database version => replace local database with remote database
                 If the local database revision = remote database version => do nothing */
              if (successfullyDecryptedDb) {
                  if (database.getRevision() > remoteDatabase.getRevision()) {
                      transport.delete(remoteLocation, database.getDatabaseFile().getName(), httpUsername, httpPassword);
                      transport.put(remoteLocation, database.getDatabaseFile(), httpUsername, httpPassword);
                      syncSuccessful = true;
                  } else if (database.getRevision() < remoteDatabase.getRevision()) {
                      Util.copyFile(remoteDatabaseFile, database.getDatabaseFile());
                      database = new PasswordDatabase(
                              remoteDatabase.getRevisionObj(),
                              remoteDatabase.getDbOptions(),
                              remoteDatabase.getAccountsHash(),
                              database.getDatabaseFile());
                      doOpenDatabaseActions();
                      syncSuccessful = true;
                  } else {
                      syncSuccessful = true;
                  }
  
                  if (syncSuccessful) {
                      setLocalDatabaseDirty(false);
  
                      // Create a thread that will mark the database dirty after
                      // a short period. Without this the database would remain
                      // in a synced state until the user makes a change. The
                      // longer we wait before syncing up the greater chance there
                      // is that we'll miss changes made elsewhere and end up
                      // with a conflicting version of the database.
                      final long dirtyThreadStartTime = System.currentTimeMillis();
                      runSetDBDirtyThread = true;
                      Thread setDBDirtyThread = new Thread(new Runnable() {
                          public void run() {
                              while (runSetDBDirtyThread) {
                                  try {
                                      Thread.sleep(1000);
                                  } catch (InterruptedException e1) {}
                                  long currentTime = System.currentTimeMillis();
                                  if (currentTime - dirtyThreadStartTime > 5 * 60 * 1000) {
                                      LOG.info("SetDBDirtyThread setting database dirty");
                                      setLocalDatabaseDirty(true);
                                      runSetDBDirtyThread = false;
                                  }
                              }
                          }
                      });
                      setDBDirtyThread.setName("SetDBDirty");
                      setDBDirtyThread.start();
                      LOG.info("Started SetDBDirtyThread thread");
  
                  }
              }
  
          } finally {
              mainWindow.getContentPane().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
              fileMonitor.start();
          }
  
          return syncSuccessful;
  
      }
  
  
      public void exitApplication() {
          System.exit(0);
      }
  
  
      public void export() {
          File exportFile = getSaveAsFile(Translator.translate("exportFile"));
          if (exportFile == null) {
              return;
          }
  
          if (exportFile.exists()) {
              exportFile.delete();
          }
  
          AccountsCSVMarshaller marshaller = new AccountsCSVMarshaller();
          try {
              marshaller.marshal(this.database.getAccounts(), exportFile);
          } catch (ExportException e) {
              JOptionPane.showMessageDialog(mainWindow, e.getMessage(), Translator.translate("problemExporting"), JOptionPane.ERROR_MESSAGE);
          }
      }
  
  
      public void importAccounts() throws TransportException, ProblemReadingDatabaseFile, IOException, CryptoException, PasswordDatabaseException {
          if (getLatestVersionOfDatabase()) {
              // Prompt for the file to import
              JFileChooser fc = new JFileChooser();
              fc.setDialogTitle(Translator.translate("import"));
              int returnVal = fc.showOpenDialog(mainWindow);
  
              if (returnVal == JFileChooser.APPROVE_OPTION) {
                  File csvFile = fc.getSelectedFile();
  
                  // Unmarshall the accounts from the CSV file
                  try {
                      AccountsCSVMarshaller marshaller = new AccountsCSVMarshaller();
                      ArrayList accountsInCSVFile = marshaller.unmarshal(csvFile);
                      ArrayList accountsToImport = new ArrayList();
  
                      boolean importCancelled = false;
                      // Add each account to the open database. If the account
                      // already exits the prompt to overwrite
                      for (int i=0; i<accountsInCSVFile.size(); i++) {
                          AccountInformation importedAccount = (AccountInformation) accountsInCSVFile.get(i);
                          if (database.getAccount(importedAccount.getAccountName()) != null) {
                              Object[] options = {"Overwrite Existing", "Keep Existing", "Cancel"};
                              int answer = JOptionPane.showOptionDialog(
                                      mainWindow,
                                      Translator.translate("importExistingQuestion", importedAccount.getAccountName()),
                                      Translator.translate("importExistingTitle"),
                                      JOptionPane.YES_NO_CANCEL_OPTION,
                                      JOptionPane.QUESTION_MESSAGE,
                                      null,
                                      options,
                                      options[1]);
  
                              if (answer == 1) {
                                  continue; // If keep existing then continue to the next iteration
                              } else if (answer == 2) {
                                  importCancelled = true;
                                  break; // Cancel the import
                              }
                          }
  
                          accountsToImport.add(importedAccount);
                      }
  
                      if (!importCancelled && accountsToImport.size() > 0) {
                          for (int i=0; i<accountsToImport.size(); i++) {
                              AccountInformation accountToImport = (AccountInformation) accountsToImport.get(i);
                              database.deleteAccount(accountToImport.getAccountName());
                              database.addAccount(accountToImport);
                          }
                          saveDatabase();
                          accountNames = getAccountNames();
                          filter();
                      }
  
                  } catch (ImportException e) {
                      JOptionPane.showMessageDialog(mainWindow, e.getMessage(), Translator.translate("problemImporting"), JOptionPane.ERROR_MESSAGE);
                  } catch (IOException e) {
                      JOptionPane.showMessageDialog(mainWindow, e.getMessage(), Translator.translate("problemImporting"), JOptionPane.ERROR_MESSAGE);
                  } catch (CryptoException e) {
                      JOptionPane.showMessageDialog(mainWindow, e.getMessage(), Translator.translate("problemImporting"), JOptionPane.ERROR_MESSAGE);
                  }
              }
          }
      }
  
  
      /**
       * This method prompts the user for the name of a file.
       * If the file exists then it will ask if they want to overwrite (the file isn't overwritten though,
       * that would be done by the calling method)
       * @param title The string title to put on the dialog
       * @return The file to save to or null
       */
      private File getSaveAsFile(String title) {
          File selectedFile;
  
          boolean gotValidFile = false;
          do {
              JFileChooser fc = new JFileChooser();
              fc.setDialogTitle(title);
              int returnVal = fc.showSaveDialog(mainWindow);
  
              if (returnVal != JFileChooser.APPROVE_OPTION) {
                  return null;
              }
  
              selectedFile = fc.getSelectedFile();
  
              //Warn the user if the database file already exists
              if (selectedFile.exists()) {
                  Object[] options = {"Yes", "No"};
                  int i = JOptionPane.showOptionDialog(mainWindow,
                          Translator.translate("fileAlreadyExistsWithFileName", selectedFile.getAbsolutePath()) + '\n' +
                              Translator.translate("overwrite"),
                              Translator.translate("fileAlreadyExists"),
                              JOptionPane.YES_NO_OPTION,
                              JOptionPane.QUESTION_MESSAGE,
                              null,
                              options,
                              options[1]);
                  if (i == JOptionPane.YES_OPTION) {
                      gotValidFile = true;
                  }
              } else {
                  gotValidFile = true;
              }
  
          } while (!gotValidFile);
  
          return selectedFile;
      }
  
  
      private void saveDatabase() throws IOException, CryptoException {
          dbPers.save(database);
          if (fileMonitor != null) {
              fileMonitor.start();
          }
          if (databaseHasRemoteInstance()) {
              setLocalDatabaseDirty(true);
          } else {
              setLocalDatabaseDirty(false);
          }
      }
  
  
      private void setLocalDatabaseDirty(boolean dirty) {
          localDatabaseDirty = dirty;
  
          if (databaseHasRemoteInstance()) {
              if (localDatabaseDirty) {
                  mainWindow.getSyncWithRemoteDatabaseMenuItem().setEnabled(true);
                  mainWindow.getSyncWithRemoteDatabaseButton().setEnabled(true);
              } else {
                  mainWindow.getSyncWithRemoteDatabaseMenuItem().setEnabled(false);
                  mainWindow.getSyncWithRemoteDatabaseButton().setEnabled(false);
              }
          } else {
              mainWindow.getSyncWithRemoteDatabaseMenuItem().setEnabled(false);
              mainWindow.getSyncWithRemoteDatabaseButton().setEnabled(false);
          }
  
          setStatusBarText();
      }
  
  
      private void setStatusBarText() {
          String status = null;
          Color color = null;
          if (databaseHasRemoteInstance()) {
              if (localDatabaseDirty) {
                  status = Translator.translate("unsynchronised");
                  color = Color.RED;
              } else {
                  status = Translator.translate("synchronised");
                  color = Color.BLACK;
              }
              status = Translator.translate("revision") + ' ' + String.valueOf(database.getRevision()) + " - " + status;
          } else {
              status = Translator.translate("localDatabase");
              color = Color.BLACK;
          }
          mainWindow.getStatusBar().setText(status);
          mainWindow.getStatusBar().setForeground(color);
      }
  
  
      private class AutoLockDatabaseListener implements WindowFocusListener {
  
          private String databaseClosedOnTimer;
          private Timer closeDBTimer;
  
          public synchronized void windowGainedFocus(WindowEvent we) {
              if (closeDBTimer != null) {
                  LOG.debug("Stopping closeDBTimer");
                  closeDBTimer.removeActionListener(
                          closeDBTimer.getActionListeners()[0]);
                  closeDBTimer = null;
              }
              if (databaseClosedOnTimer != null) {
                  try {
                      openDatabase(databaseClosedOnTimer);
                  } catch (Exception e) {
                      errorHandler(e);
                  }
                  databaseClosedOnTimer = null;
              }
          }
  
          /**
           * If the app loses focus, there's an open db and there's no closeDBTimer
           * already registered then start a timer to close the database after the
           * configured number of minutes.
           */
          public synchronized void windowLostFocus(WindowEvent e) {
              // If the window receiving focus is within this application then the
              // app isn't not losing focus so no further action is required.
              if (e.getOppositeWindow() != null &&
                      e.getOppositeWindow().getOwner() == mainWindow) {
                  LOG.debug("Focus switched to another window within this app");
                  return;
              }
  
              if (database != null && closeDBTimer == null){
                  closeDBTimer = new Timer(msToWaitBeforeClosingDB , null);
                  closeDBTimer.addActionListener(new ActionListener() {
                      public void actionPerformed(ActionEvent e) {
                          LOG.debug("Closing database due to inactivity");
                          databaseClosedOnTimer =
                                  database.getDatabaseFile().getAbsolutePath();
                          doCloseDatabaseActions();
                          database = null;
                          closeDBTimer = null;
                      }
                  });
                  closeDBTimer.setRepeats(false);
                  closeDBTimer.start();
                  LOG.debug("Started lost focus timer, " + msToWaitBeforeClosingDB);
              }
          }
  
      }
  }
  