1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.store

File FileSaveTransactionRunnable.java

 

Coverage histogram

../../../img/srcFileCovDistChart6.png
69% of files have more coverage

Code metrics

30
58
9
1
302
136
31
0.53
6.44
9
3.44

Classes

Class Line # Actions
FileSaveTransactionRunnable 39 58 0% 31 42
0.567010356.7%
 

Contributing tests

This file is covered by 11 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.store;
21   
22    import java.io.File;
23    import java.io.FileOutputStream;
24    import java.io.IOException;
25    import java.io.InputStream;
26    import java.io.OutputStream;
27    import java.util.concurrent.locks.ReadWriteLock;
28   
29    import org.apache.commons.io.IOUtils;
30   
31    /**
32    * A TransactionRunnable for saving a file safely.
33    * The operation can be rolled back even after the onCommit() function is called.
34    * It is only final when the onComplete function is called.
35    *
36    * @version $Id: 90e8613fe32bc74b616ee4fe15ee44e25f6e9837 $
37    * @since 3.0M2
38    */
 
39    public class FileSaveTransactionRunnable extends StartableTransactionRunnable<TransactionRunnable>
40    {
41    /**
42    * The location of the file to save the attachment content in.
43    */
44    private final File toSave;
45   
46    /**
47    * The location of the temporary file.
48    */
49    private final File tempFile;
50   
51    /**
52    * The location of the backup file.
53    */
54    private final File backupFile;
55   
56    /**
57    * A lock to hold while running this TransactionRunnable.
58    */
59    private final ReadWriteLock lock;
60   
61    /**
62    * The source of the data to save.
63    */
64    private final StreamProvider provider;
65   
66    /**
67    * False until run() has complete. If false then we know there is nothing to rollback and
68    * more importantly, we do not know if files in the temporary and backup locations are not
69    * from a previous (catastrophically failed) save operation.
70    */
71    private boolean runComplete;
72   
73    /**
74    * The Constructor.
75    *
76    * @param toSave the file to put the content in.
77    * @param tempFile a temporary file, this should not contain anything important as it will be deleted
78    * and must not be altered while the operation is running. This will contain the data
79    * until onCommit when it is renamed to the toSave file.
80    * @param backupFile a temporary file, this should not contain anything important as it will be deleted
81    * and must not be altered while the operation is running. This will contain whatever
82    * was in the toSave file prior, just in case onRollback must be called.
83    * @param lock a ReadWriteLock whose writeLock will be locked as the beginning of the process and
84    * unlocked when complete.
85    * @param provider a StreamProvider to get the data to put into the file.
86    */
 
87  21 toggle public FileSaveTransactionRunnable(final File toSave,
88    final File tempFile,
89    final File backupFile,
90    final ReadWriteLock lock,
91    final StreamProvider provider)
92    {
93  21 this.toSave = toSave;
94  21 this.tempFile = tempFile;
95  21 this.backupFile = backupFile;
96  21 this.lock = lock;
97  21 this.provider = provider;
98    }
99   
100    /**
101    * {@inheritDoc}
102    * <p>
103    * Obtain the lock and make sure the temporary and backup files are deleted.
104    * </p>
105    *
106    * @see TransactionRunnable#preRun()
107    */
 
108  21 toggle protected void onPreRun() throws IOException
109    {
110  21 this.lock.writeLock().lock();
111  21 this.clearTempAndBackup();
112    }
113   
114    /**
115    * {@inheritDoc}
116    * <p>
117    * Write the data from the provider to the temporary file.
118    * </p>
119    *
120    * @see TransactionRunnable#run()
121    */
 
122  20 toggle protected void onRun() throws Exception
123    {
124  20 if (!this.toSave.getParentFile().exists() && !this.toSave.getParentFile().mkdirs()) {
125  0 throw new IOException("Could not make directory tree to place file in. "
126    + "Do you have permission to write to ["
127    + this.toSave.getAbsolutePath() + "] ?");
128    }
129   
130  20 final InputStream in = this.provider.getStream();
131  20 try {
132  20 final OutputStream out = new FileOutputStream(this.tempFile);
133  20 try {
134  20 IOUtils.copy(in, out);
135    } finally {
136  20 out.close();
137    }
138    } finally {
139  20 this.runComplete = true;
140  20 in.close();
141    }
142    }
143   
144    /**
145    * {@inheritDoc}
146    * <p>
147    * Move whatever is in the main file location into backup and move
148    * the temp file into the main location.
149    * </p>
150    *
151    * @see TransactionRunnable#onCommit()
152    */
 
153  18 toggle protected void onCommit()
154    {
155  18 if (this.toSave.exists()) {
156  2 this.toSave.renameTo(this.backupFile);
157    }
158  18 this.tempFile.renameTo(this.toSave);
159    }
160   
 
161  2 toggle protected void onRollback()
162    {
163    // If this is false then we know run() has not yet happened and we know there is nothing to do.
164  2 if (this.runComplete) {
165  2 if (this.tempFile.exists()) {
166  2 this.onRollbackWithTempFile();
167    } else {
168  0 this.onRollbackNoTempFile();
169    }
170    }
171    }
172   
173    /**
174    * {@inheritDoc}
175    * <p>
176    * Once this is called, there is no going back.
177    * Remove temporary and backup files and unlock the lock.
178    * </p>
179    *
180    * @see TransactionRunnable#onComplete()
181    */
 
182  21 toggle protected void onComplete() throws IOException
183    {
184  21 try {
185  21 this.clearTempAndBackup();
186    } finally {
187  21 this.lock.writeLock().unlock();
188    }
189    }
190   
191    /**
192    * Knowing there is no temp file, we can determine that one of 4 possible things happened.
193    *
194    * 1. No backup file and no main file. There was probably no file to begin with
195    * and it failed before anything could be saved in the temp file. Do nothing.
196    *
197    * 2. No backup file but there is a main file, assume onCommit happened successfully but there
198    * was no file here to begin with so there was nothing to back up. Move the main file back
199    * to the temporary location.
200    *
201    * 3. If there is a backup file, but no main file, this is unexpected but since the backup file
202    * should be the previous main file, move it back to the main location and log a warning
203    * that the storage engine encountered an unexpected albeit probably recoverable state.
204    *
205    * 4. If there is a backup file and a main file, onCommit probably went smoothly and a problem
206    * was encountered somewhere else forcing the rollback. Move the main file back to the
207    * temporary location and the backup file back to the main location.
208    */
 
209  0 toggle private void onRollbackNoTempFile()
210    {
211  0 boolean isBackupFile = this.backupFile.exists();
212  0 boolean isMainFile = this.toSave.exists();
213   
214    // 1.
215  0 if (!isBackupFile && !isMainFile) {
216  0 return;
217    }
218   
219    // 2.
220  0 if (!isBackupFile && isMainFile) {
221  0 this.toSave.renameTo(this.tempFile);
222  0 return;
223    }
224   
225    // 3.
226  0 if (isBackupFile && !isMainFile) {
227  0 this.backupFile.renameTo(this.toSave);
228    // TODO log a low severity warning.
229  0 return;
230    }
231   
232    // 4.
233  0 if (isBackupFile && isMainFile) {
234  0 this.toSave.renameTo(this.tempFile);
235  0 this.backupFile.renameTo(this.toSave);
236  0 return;
237    }
238    }
239   
240    /**
241    * Knowing there is a temp file, one of 3 things might have happened:
242    *
243    * 1. If there is no backup file, assume onCommit did not occur, do nothing regardless
244    * of whether there is or isn't an (existing) main file.
245    *
246    * 2. If there is a backup file but no main file, there must have been a failure half way
247    * through onCommit, it was able to move the existing main file to the backup
248    * location but did not move the temporary file to the main location. Move the backup file
249    * back to the main location.
250    *
251    * 3. If there is a file in each location which should not happen and if it does,
252    * throw an exception which will be printed in the log.
253    */
 
254  2 toggle private void onRollbackWithTempFile()
255    {
256  2 boolean isBackupFile = this.backupFile.exists();
257  2 boolean isMainFile = this.toSave.exists();
258   
259    // 1.
260  2 if (!isBackupFile) {
261  2 return;
262    }
263   
264    // 2.
265  0 if (isBackupFile && !isMainFile) {
266  0 this.backupFile.renameTo(this.toSave);
267  0 return;
268    }
269   
270    // 3.
271  0 if (isBackupFile && isMainFile) {
272  0 throw new IllegalStateException("Tried to rollback the saving of file "
273    + this.toSave.getAbsolutePath() + " and encountered a "
274    + "backup, a temp file, and a main file. Since any existing "
275    + "main file is renamed to a temp location and the content is "
276    + "saved in the backup location and then renamed to the main "
277    + "location, the existance of all 3 at once should never "
278    + "happen.");
279    }
280    }
281   
282    /**
283    * Remove temporary and backup files.
284    *
285    * @throws IOException if removing files fails or files still exist after delete() is called.
286    */
 
287  42 toggle private void clearTempAndBackup() throws IOException
288    {
289  42 if (this.tempFile.exists()) {
290  7 this.tempFile.delete();
291    }
292  42 if (this.tempFile.exists()) {
293  0 throw new IOException("Could not remove temporary file, cannot proceed.");
294    }
295  42 if (this.backupFile.exists()) {
296  7 this.backupFile.delete();
297    }
298  42 if (this.backupFile.exists()) {
299  0 throw new IOException("Could not remove backup file, cannot proceed.");
300    }
301    }
302    }