package net.sf.jabref.export; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.forms.layout.FormLayout; import net.sf.jabref.*; import net.sf.jabref.gui.FileDialogs; import net.sf.jabref.collab.ChangeScanner; import javax.swing.*; import java.io.File; import java.io.IOException; import java.nio.charset.UnsupportedCharsetException; import java.util.Vector; /** * Action for the "Save" and "Save as" operations called from BasePanel. This class is also used for * save operations when closing a database or quitting the applications. * * The operations run synchronously, but offload the save operation from the event thread using Spin. * Callers can query whether the operation was cancelled, or whether it was successful. */ public class SaveDatabaseAction extends AbstractWorker { protected BasePanel panel; private JabRefFrame frame; private boolean success = false, cancelled = false, fileLockedError = false; public SaveDatabaseAction(BasePanel panel) { this.panel = panel; this.frame = panel.frame(); } public void init() throws Throwable { success = false; cancelled = false; fileLockedError = false; if (panel.getFile() == null) saveAs(); else { // Check for external modifications: if (panel.isUpdatedExternally() || Globals.fileUpdateMonitor.hasBeenModified(panel.getFileMonitorHandle())) { String[] opts = new String[]{Globals.lang("Review changes"), Globals.lang("Save"), Globals.lang("Cancel")}; int answer = JOptionPane.showOptionDialog(panel.frame(), Globals.lang("File has been updated externally. " + "What do you want to do?"), Globals.lang("File updated externally"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, opts, opts[0]); /* int choice = JOptionPane.showConfirmDialog(frame, Globals.lang("File has been updated externally. " +"Are you sure you want to save?"), Globals.lang("File updated externally"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);*/ if (answer == JOptionPane.CANCEL_OPTION) { cancelled = true; return; } else if (answer == JOptionPane.YES_OPTION) { //try { cancelled = true; (new Thread(new Runnable() { public void run() { if (!Util.waitForFileLock(panel.getFile(), 10)) { // TODO: GUI handling of the situation when the externally modified file keeps being locked. System.err.println("File locked, this will be trouble."); } ChangeScanner scanner = new ChangeScanner(panel.frame(), panel); scanner.changeScan(panel.getFile()); try { scanner.join(); } catch (InterruptedException e) { e.printStackTrace(); } if (scanner.changesFound()) { scanner.displayResult(new ChangeScanner.DisplayResultCallback() { public void scanResultsResolved(boolean resolved) { if (!resolved) { cancelled = true; } else { panel.setUpdatedExternally(false); SwingUtilities.invokeLater(new Runnable() { public void run() { panel.getSidePaneManager().hide("fileUpdate"); } }); } } }); } } })).start(); return; } else { // User indicated to store anyway. // See if the database has the protected flag set: Vector<String> pd = panel.metaData().getData(Globals.PROTECTED_FLAG_META); boolean databaseProtectionFlag = (pd != null) && Boolean.parseBoolean(pd.get(0)); if (databaseProtectionFlag) { JOptionPane.showMessageDialog(frame, Globals.lang("Database is protected. Cannot save until external changes have been reviewed."), Globals.lang("Protected database"), JOptionPane.ERROR_MESSAGE); cancelled = true; } else { panel.setUpdatedExternally(false); panel.getSidePaneManager().hide("fileUpdate"); } } } panel.frame().output(Globals.lang("Saving database") + "..."); panel.setSaving(true); } } public void update() { if (success) { // Reset title of tab frame.setTabTitle(panel, panel.getFile().getName(), panel.getFile().getAbsolutePath()); frame.output(Globals.lang("Saved database") + " '" + panel.getFile().getPath() + "'."); frame.setWindowTitle(); } else if (!cancelled) { if (fileLockedError) { // TODO: user should have the option to override the lock file. frame.output(Globals.lang("Could not save, file locked by another JabRef instance.")); } else frame.output(Globals.lang("Save failed")); } } public void run() { if (cancelled || (panel.getFile() == null)) { return; } try { // Make sure the current edit is stored: panel.storeCurrentEdit(); // If the option is set, autogenerate keys for all entries that are // lacking keys, before saving: panel.autoGenerateKeysBeforeSaving(); if (!Util.waitForFileLock(panel.getFile(), 10)) { success = false; fileLockedError = true; } else { // Now save the database: success = saveDatabase(panel.getFile(), false, panel.getEncoding()); //Util.pr("Testing resolve string... BasePanel line 237"); //Util.pr("Resolve aq: "+database.resolveString("aq")); //Util.pr("Resolve text: "+database.resolveForStrings("A text which refers to the string #aq# and #billball#, hurra.")); try { Globals.fileUpdateMonitor.updateTimeStamp(panel.getFileMonitorHandle()); } catch (IllegalArgumentException ex) { // This means the file has not yet been registered, which is the case // when doing a "Save as". Maybe we should change the monitor so no // exception is cast. } } panel.setSaving(false); if (success) { panel.undoManager.markUnchanged(); if (!AutoSaveManager.deleteAutoSaveFile(panel)) { //System.out.println("Deletion of autosave file failed"); }/* else System.out.println("Deleted autosave file (if it existed)");*/ // (Only) after a successful save the following // statement marks that the base is unchanged // since last save: panel.setNonUndoableChange(false); panel.setBaseChanged(false); panel.setUpdatedExternally(false); } } catch (SaveException ex2) { if (ex2 == SaveException.FILE_LOCKED) { success =false; fileLockedError = true; return; } ex2.printStackTrace(); } } private boolean saveDatabase(File file, boolean selectedOnly, String encoding) throws SaveException { SaveSession session; frame.block(); try { if (!selectedOnly) session = FileActions.saveDatabase(panel.database(), panel.metaData(), file, Globals.prefs, false, false, encoding, false); else session = FileActions.savePartOfDatabase(panel.database(), panel.metaData(), file, Globals.prefs, panel.getSelectedEntries(), encoding); } catch (UnsupportedCharsetException ex2) { JOptionPane.showMessageDialog(frame, Globals.lang("Could not save file. " + "Character encoding '%0' is not supported.", encoding), Globals.lang("Save database"), JOptionPane.ERROR_MESSAGE); throw new SaveException("rt"); } catch (SaveException ex) { if (ex == SaveException.FILE_LOCKED) { throw ex; } if (ex.specificEntry()) { // Error occured during processing of // be. Highlight it: int row = panel.mainTable.findEntry(ex.getEntry()), topShow = Math.max(0, row - 3); panel.mainTable.setRowSelectionInterval(row, row); panel.mainTable.scrollTo(topShow); panel.showEntry(ex.getEntry()); } else ex.printStackTrace(); JOptionPane.showMessageDialog (frame, Globals.lang("Could not save file") + ".\n" + ex.getMessage(), Globals.lang("Save database"), JOptionPane.ERROR_MESSAGE); throw new SaveException("rt"); } finally { frame.unblock(); } boolean commit = true; if (!session.getWriter().couldEncodeAll()) { DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("left:pref, 4dlu, fill:pref", "")); JTextArea ta = new JTextArea(session.getWriter().getProblemCharacters()); ta.setEditable(false); builder.append(Globals.lang("The chosen encoding '%0' could not encode the following characters: ", session.getEncoding())); builder.append(ta); builder.append(Globals.lang("What do you want to do?")); String tryDiff = Globals.lang("Try different encoding"); int answer = JOptionPane.showOptionDialog(frame, builder.getPanel(), Globals.lang("Save database"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, new String[]{Globals.lang("Save"), tryDiff, Globals.lang("Cancel")}, tryDiff); if (answer == JOptionPane.NO_OPTION) { // The user wants to use another encoding. Object choice = JOptionPane.showInputDialog(frame, Globals.lang("Select encoding"), Globals.lang("Save database"), JOptionPane.QUESTION_MESSAGE, null, Globals.ENCODINGS, encoding); if (choice != null) { String newEncoding = (String) choice; return saveDatabase(file, selectedOnly, newEncoding); } else commit = false; } else if (answer == JOptionPane.CANCEL_OPTION) commit = false; } try { if (commit) { session.commit(); panel.setEncoding(encoding); // Make sure to remember which encoding we used. } else session.cancel(); } catch (SaveException e) { int ans = JOptionPane.showConfirmDialog(null, Globals.lang("Save failed during backup creation")+". " +Globals.lang("Save without backup?"), Globals.lang("Unable to create backup"), JOptionPane.YES_NO_OPTION); if (ans == JOptionPane.YES_OPTION) { session.setUseBackup(false); session.commit(); panel.setEncoding(encoding); } else commit = false; } catch (IOException e) { e.printStackTrace(); } return commit; } /** * Run the "Save" operation. This method offloads the actual save operation to a background thread, but * still runs synchronously using Spin (the method returns only after completing the operation). */ public void runCommand() throws Throwable { // This part uses Spin's features: Worker wrk = getWorker(); // The Worker returned by getWorker() has been wrapped // by Spin.off(), which makes its methods be run in // a different thread from the EDT. CallBack clb = getCallBack(); init(); // This method runs in this same thread, the EDT. // Useful for initial GUI actions, like printing a message. // The CallBack returned by getCallBack() has been wrapped // by Spin.over(), which makes its methods be run on // the EDT. wrk.run(); // Runs the potentially time-consuming action // without freezing the GUI. The magic is that THIS line // of execution will not continue until run() is finished. clb.update(); // Runs the update() method on the EDT. } public void save() throws Throwable { runCommand(); } /** * Run the "Save as" operation. This method offloads the actual save operation to a background thread, but * still runs synchronously using Spin (the method returns only after completing the operation). */ public void saveAs() throws Throwable { String chosenFile = null; File f = null; while (f == null) { chosenFile = FileDialogs.getNewFile(frame.getFrame(), new File(Globals.prefs.get("workingDirectory")), ".bib", JFileChooser.SAVE_DIALOG, false, null); if (chosenFile == null) { cancelled = true; return; // cancelled } f = new File(chosenFile); // Check if the file already exists: if (f.exists() && (JOptionPane.showConfirmDialog (frame, "'" + f.getName() + "' " + Globals.lang("exists. Overwrite file?"), Globals.lang("Save database"), JOptionPane.OK_CANCEL_OPTION) != JOptionPane.OK_OPTION)) { f = null; } } if (chosenFile != null) { File oldFile = panel.metaData().getFile(); panel.metaData().setFile(f); Globals.prefs.put("workingDirectory", f.getParent()); runCommand(); // If the operation failed, revert the file field and return: if (!success) { panel.metaData().setFile(oldFile); return; } // Register so we get notifications about outside changes to the file. try { panel.setFileMonitorHandle(Globals.fileUpdateMonitor.addUpdateListener(panel, panel.getFile())); } catch (IOException ex) { ex.printStackTrace(); } frame.getFileHistory().newFile(panel.metaData().getFile().getPath()); } } /** * Query whether the last operation was successful. * * @returns true if the last Save/SaveAs operation completed successfully, false otherwise. */ public boolean isSuccess() { return success; } /** * Query whether the last operation was cancelled. * * @returns true if the last Save/SaveAs operation was cancelled from the file dialog or from another * query dialog, false otherwise. */ public boolean isCancelled() { return cancelled; } }