bcc1f411c7c375ec2870b7671b2c5a555c10ac96
[debian/jabref.git] / src / main / java / net / sf / jabref / export / SaveDatabaseAction.java
1 /*  Copyright (C) 2003-2011 JabRef contributors.
2     This program is free software; you can redistribute it and/or modify
3     it under the terms of the GNU General Public License as published by
4     the Free Software Foundation; either version 2 of the License, or
5     (at your option) any later version.
6
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10     GNU General Public License for more details.
11
12     You should have received a copy of the GNU General Public License along
13     with this program; if not, write to the Free Software Foundation, Inc.,
14     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
15 */
16 package net.sf.jabref.export;
17
18 import com.jgoodies.forms.builder.DefaultFormBuilder;
19 import com.jgoodies.forms.layout.FormLayout;
20 import net.sf.jabref.*;
21 import net.sf.jabref.gui.FileDialogs;
22 import net.sf.jabref.collab.ChangeScanner;
23
24 import javax.swing.*;
25 import java.io.File;
26 import java.io.IOException;
27 import java.nio.charset.UnsupportedCharsetException;
28 import java.util.Vector;
29
30 /**
31  * Action for the "Save" and "Save as" operations called from BasePanel. This class is also used for
32  * save operations when closing a database or quitting the applications.
33  *
34  * The operations run synchronously, but offload the save operation from the event thread using Spin.
35  * Callers can query whether the operation was cancelled, or whether it was successful.
36  */
37 public class SaveDatabaseAction extends AbstractWorker {
38     private BasePanel panel;
39     private JabRefFrame frame;
40     private boolean success = false, cancelled = false, fileLockedError = false;
41
42     public SaveDatabaseAction(BasePanel panel) {
43
44         this.panel = panel;
45         this.frame = panel.frame();
46     }
47
48
49     public void init() throws Throwable {
50         success = false;
51         cancelled = false;
52         fileLockedError = false;
53         if (panel.getFile() == null)
54             saveAs();
55         else {
56
57             // Check for external modifications:
58             if (panel.isUpdatedExternally() || Globals.fileUpdateMonitor.hasBeenModified(panel.getFileMonitorHandle())) {
59
60                 String[] opts = new String[]{Globals.lang("Review changes"), Globals.lang("Save"),
61                         Globals.lang("Cancel")};
62                 int answer = JOptionPane.showOptionDialog(panel.frame(), Globals.lang("File has been updated externally. "
63                         + "What do you want to do?"), Globals.lang("File updated externally"),
64                         JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE,
65                         null, opts, opts[0]);
66                 //  int choice = JOptionPane.showConfirmDialog(frame, Globals.lang("File has been updated externally. "
67                 // +"Are you sure you want to save?"), Globals.lang("File updated externally"),
68                 // JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
69
70                 if (answer == JOptionPane.CANCEL_OPTION) {
71                     cancelled = true;
72                     return;
73                 }
74                 else if (answer == JOptionPane.YES_OPTION) {
75                     //try {
76
77                     cancelled = true;
78
79                     (new Thread(new Runnable() {
80                         public void run() {
81
82                             if (!Util.waitForFileLock(panel.getFile(), 10)) {
83                                 // TODO: GUI handling of the situation when the externally modified file keeps being locked.
84                                 System.err.println("File locked, this will be trouble.");
85                             }
86
87                             ChangeScanner scanner = new ChangeScanner(panel.frame(), panel);
88                             scanner.changeScan(panel.getFile());
89                             try {
90                                 scanner.join();
91                             } catch (InterruptedException e) {
92                                 e.printStackTrace();
93                             }
94                             if (scanner.changesFound()) {
95                                 scanner.displayResult(new ChangeScanner.DisplayResultCallback() {
96                                     public void scanResultsResolved(boolean resolved) {
97                                         if (!resolved) {
98                                             cancelled = true;
99                                         } else {
100                                             panel.setUpdatedExternally(false);
101                                             SwingUtilities.invokeLater(new Runnable() {
102                                                 public void run() {
103                                                     panel.getSidePaneManager().hide("fileUpdate");
104                                                 }
105                                             });
106                                         }
107                                     }
108                                 });
109                             }
110                         }
111                     })).start();
112
113                     return;
114                 }
115                 else { // User indicated to store anyway.
116                     // See if the database has the protected flag set:
117                     Vector<String> pd = panel.metaData().getData(Globals.PROTECTED_FLAG_META);
118                     boolean databaseProtectionFlag = (pd != null) && Boolean.parseBoolean(pd.get(0));
119                     if (databaseProtectionFlag) {
120                         JOptionPane.showMessageDialog(frame, Globals.lang("Database is protected. Cannot save until external changes have been reviewed."),
121                                 Globals.lang("Protected database"), JOptionPane.ERROR_MESSAGE);
122                         cancelled = true;
123                     }
124                     else {
125                         panel.setUpdatedExternally(false);
126                         panel.getSidePaneManager().hide("fileUpdate");
127                     }
128                 }
129             }
130
131             panel.frame().output(Globals.lang("Saving database") + "...");
132             panel.setSaving(true);
133         }
134     }
135
136     public void update() {
137         if (success) {
138             // Reset title of tab
139             frame.setTabTitle(panel, panel.getFile().getName(),
140                     panel.getFile().getAbsolutePath());
141             frame.output(Globals.lang("Saved database") + " '"
142                     + panel.getFile().getPath() + "'.");
143             frame.setWindowTitle();
144         } else if (!cancelled) {
145             if (fileLockedError) {
146                 // TODO: user should have the option to override the lock file.
147                 frame.output(Globals.lang("Could not save, file locked by another JabRef instance."));
148             } else
149                 frame.output(Globals.lang("Save failed"));
150         }
151     }
152
153     public void run() {
154         if (cancelled || (panel.getFile() == null)) {
155             return;
156         }
157
158         try {
159
160             // Make sure the current edit is stored:
161             panel.storeCurrentEdit();
162
163             // If the option is set, autogenerate keys for all entries that are
164             // lacking keys, before saving:
165             panel.autoGenerateKeysBeforeSaving();
166
167             if (!Util.waitForFileLock(panel.getFile(), 10)) {
168                 success = false;
169                 fileLockedError = true;
170             }
171             else {
172                 // Now save the database:
173                 success = saveDatabase(panel.getFile(), false, panel.getEncoding());
174
175                 //Util.pr("Testing resolve string... BasePanel line 237");
176                 //Util.pr("Resolve aq: "+database.resolveString("aq"));
177                 //Util.pr("Resolve text: "+database.resolveForStrings("A text which refers to the string #aq# and #billball#, hurra."));
178
179                 try {
180                     Globals.fileUpdateMonitor.updateTimeStamp(panel.getFileMonitorHandle());
181                 } catch (IllegalArgumentException ex) {
182                     // This means the file has not yet been registered, which is the case
183                     // when doing a "Save as". Maybe we should change the monitor so no
184                     // exception is cast.
185                 }
186             }
187             panel.setSaving(false);
188             if (success) {
189                 panel.undoManager.markUnchanged();
190
191                 if (!AutoSaveManager.deleteAutoSaveFile(panel)) {
192                     //System.out.println("Deletion of autosave file failed");
193                 }/* else
194                     System.out.println("Deleted autosave file (if it existed)");*/
195                 // (Only) after a successful save the following
196                 // statement marks that the base is unchanged
197                 // since last save:
198                 panel.setNonUndoableChange(false);
199                 panel.setBaseChanged(false);
200                 panel.setUpdatedExternally(false);
201             }
202         } catch (SaveException ex2) {
203             if (ex2 == SaveException.FILE_LOCKED) {
204                 success =false;
205                 fileLockedError = true;
206                 return;
207             }
208             ex2.printStackTrace();
209         }
210     }
211
212     private boolean saveDatabase(File file, boolean selectedOnly, String encoding) throws SaveException {
213         SaveSession session;
214         frame.block();
215         try {
216             if (!selectedOnly)
217                 session = FileActions.saveDatabase(panel.database(), panel.metaData(), file,
218                         Globals.prefs, false, false, encoding, false);
219             else
220                 session = FileActions.savePartOfDatabase(panel.database(), panel.metaData(), file,
221                         Globals.prefs, panel.getSelectedEntries(), encoding);
222
223         } catch (UnsupportedCharsetException ex2) {
224             JOptionPane.showMessageDialog(frame, Globals.lang("Could not save file. "
225                     + "Character encoding '%0' is not supported.", encoding),
226                     Globals.lang("Save database"), JOptionPane.ERROR_MESSAGE);
227             throw new SaveException("rt");
228         } catch (SaveException ex) {
229             if (ex == SaveException.FILE_LOCKED) {
230                 throw ex;
231             }
232             if (ex.specificEntry()) {
233                 // Error occured during processing of
234                 // be. Highlight it:
235                 int row = panel.mainTable.findEntry(ex.getEntry()),
236                         topShow = Math.max(0, row - 3);
237                 panel.mainTable.setRowSelectionInterval(row, row);
238                 panel.mainTable.scrollTo(topShow);
239                 panel.showEntry(ex.getEntry());
240             } else ex.printStackTrace();
241
242             JOptionPane.showMessageDialog
243                     (frame, Globals.lang("Could not save file")
244                             + ".\n" + ex.getMessage(),
245                             Globals.lang("Save database"),
246                             JOptionPane.ERROR_MESSAGE);
247             throw new SaveException("rt");
248
249         } finally {
250             frame.unblock();
251         }
252
253         boolean commit = true;
254         if (!session.getWriter().couldEncodeAll()) {
255             DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("left:pref, 4dlu, fill:pref", ""));
256             JTextArea ta = new JTextArea(session.getWriter().getProblemCharacters());
257             ta.setEditable(false);
258             builder.append(Globals.lang("The chosen encoding '%0' could not encode the following characters: ",
259                     session.getEncoding()));
260             builder.append(ta);
261             builder.append(Globals.lang("What do you want to do?"));
262             String tryDiff = Globals.lang("Try different encoding");
263             int answer = JOptionPane.showOptionDialog(frame, builder.getPanel(), Globals.lang("Save database"),
264                     JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
265                     new String[]{Globals.lang("Save"), tryDiff, Globals.lang("Cancel")}, tryDiff);
266
267             if (answer == JOptionPane.NO_OPTION) {
268                 // The user wants to use another encoding.
269                 Object choice = JOptionPane.showInputDialog(frame, Globals.lang("Select encoding"), Globals.lang("Save database"),
270                         JOptionPane.QUESTION_MESSAGE, null, Globals.ENCODINGS, encoding);
271                 if (choice != null) {
272                     String newEncoding = (String) choice;
273                     return saveDatabase(file, selectedOnly, newEncoding);
274                 } else
275                     commit = false;
276             } else if (answer == JOptionPane.CANCEL_OPTION)
277                 commit = false;
278
279
280         }
281
282         try {
283             if (commit) {
284                 session.commit();
285                 panel.setEncoding(encoding); // Make sure to remember which encoding we used.
286             } else
287                 session.cancel();
288         } catch (SaveException e) {
289             int ans = JOptionPane.showConfirmDialog(null, Globals.lang("Save failed during backup creation")+". "
290                 +Globals.lang("Save without backup?"), Globals.lang("Unable to create backup"),
291                     JOptionPane.YES_NO_OPTION);
292             if (ans == JOptionPane.YES_OPTION) {
293                 session.setUseBackup(false);
294                 session.commit();
295                 panel.setEncoding(encoding);
296             }
297             else commit = false;
298         }
299
300         return commit;
301     }
302
303     /**
304      * Run the "Save" operation. This method offloads the actual save operation to a background thread, but
305      * still runs synchronously using Spin (the method returns only after completing the operation).
306      */
307     public void runCommand() throws Throwable {
308         // This part uses Spin's features:
309         Worker wrk = getWorker();
310         // The Worker returned by getWorker() has been wrapped
311         // by Spin.off(), which makes its methods be run in
312         // a different thread from the EDT.
313         CallBack clb = getCallBack();
314
315         init(); // This method runs in this same thread, the EDT.
316         // Useful for initial GUI actions, like printing a message.
317
318         // The CallBack returned by getCallBack() has been wrapped
319         // by Spin.over(), which makes its methods be run on
320         // the EDT.
321         wrk.run(); // Runs the potentially time-consuming action
322         // without freezing the GUI. The magic is that THIS line
323         // of execution will not continue until run() is finished.
324         clb.update(); // Runs the update() method on the EDT.
325
326     }
327
328     public void save() throws Throwable {
329         runCommand();
330     }
331
332     /**
333      * Run the "Save as" operation. This method offloads the actual save operation to a background thread, but
334      * still runs synchronously using Spin (the method returns only after completing the operation).
335      */
336     public void saveAs() throws Throwable {
337         String chosenFile = null;
338         File f = null;
339         while (f == null) {
340             chosenFile = FileDialogs.getNewFile(frame, new File(Globals.prefs.get("workingDirectory")), ".bib",
341                     JFileChooser.SAVE_DIALOG, false, null);
342             if (chosenFile == null) {
343                 cancelled = true;
344                 return; // cancelled
345             }
346             f = new File(chosenFile);
347             // Check if the file already exists:
348             if (f.exists() && (JOptionPane.showConfirmDialog
349                     (frame, "'" + f.getName() + "' " + Globals.lang("exists. Overwrite file?"),
350                             Globals.lang("Save database"), JOptionPane.OK_CANCEL_OPTION)
351                     != JOptionPane.OK_OPTION)) {
352                 f = null;
353             }
354         }
355
356         if (chosenFile != null) {
357             File oldFile = panel.metaData().getFile();
358             panel.metaData().setFile(f);
359             Globals.prefs.put("workingDirectory", f.getParent());
360             runCommand();
361             // If the operation failed, revert the file field and return:
362             if (!success) {
363                 panel.metaData().setFile(oldFile);
364                 return;
365             }
366             // Register so we get notifications about outside changes to the file.
367             try {
368                 panel.setFileMonitorHandle(Globals.fileUpdateMonitor.addUpdateListener(panel, panel.getFile()));
369             } catch (IOException ex) {
370                 ex.printStackTrace();
371             }
372             frame.getFileHistory().newFile(panel.metaData().getFile().getPath());
373         }
374
375     }
376
377     /**
378      * Query whether the last operation was successful.
379      *
380      * @return true if the last Save/SaveAs operation completed successfully, false otherwise.
381      */
382     public boolean isSuccess() {
383         return success;
384     }
385
386     /**
387      * Query whether the last operation was cancelled.
388      *
389      * @return true if the last Save/SaveAs operation was cancelled from the file dialog or from another
390      * query dialog, false otherwise.
391      */
392     public boolean isCancelled() {
393         return cancelled;
394     }
395 }