/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.store;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.locks.ReadWriteLock;
/**
* A TransactionRunnable for deleting a file safely.
* The operation can be rolled back even after the onCommit() function is called.
* It is only final when the onComplete function is called.
*
* @version $Id: e57fe7e5a1eee5d6734ee4ede2c2070407b0e98a $
* @since 3.0M2
*/
public class FileDeleteTransactionRunnable extends StartableTransactionRunnable<TransactionRunnable>
{
/**
* The location of the file to sdelete.
*/
private final File toDelete;
/**
* The location of the backup file.
*/
private final File backupFile;
/**
* A lock to hold while running this TransactionRunnable.
*/
private final ReadWriteLock lock;
/**
* False until preRun() has complete. If false then we know there is nothing to rollback and
* more importantly, we do not know if files in the temporary and backup locations are not
* from a previous (catastrophically failed) delete or save operation.
*/
private boolean preRunComplete;
/**
* The Constructor.
*
* @param toDelete the file to delete.
* @param backupFile a temporary file, this should not contain anything important as it will be deleted
* and must not be altered while the operation is running. This will contain whatever
* was in the toDelete file prior, just in case onRollback must be called.
* @param lock a ReadWriteLock whose writeLock will be locked as the beginning of the process and
* unlocked when complete.
*/
public FileDeleteTransactionRunnable(final File toDelete,
final File backupFile,
final ReadWriteLock lock)
{
this.toDelete = toDelete;
this.backupFile = backupFile;
this.lock = lock;
}
/**
* {@inheritDoc}
* <p>
* Obtain the lock and make sure the temporary and backup files are deleted.
* </p>
*
* @see StartableTransactionRunnable#onPreRun()
*/
@Override
protected void onPreRun() throws IOException
{
this.lock.writeLock().lock();
this.clearBackup();
this.preRunComplete = true;
}
@Override
protected void onRun() throws IOException
{
if (this.toDelete.exists()) {
this.toDelete.renameTo(this.backupFile);
}
}
/**
* {@inheritDoc}
* <p>
* There are a few possibilities. If preRun() has not completed then there may be an old backup from a previous
* delete, anyway if preRun() has not completed then we know there is nothing to rollback. Otherwise:
* </p>
* <ol>
* <li>There is a backup file but no main file, it has been renamed, rename it back to the main location.</li>
* <li>There is a main file and no backup. Nothing has probably happened, do nothing to rollback.</li>
* <li>There are neither backup nor main files, this means we tried to delete a file which didn't exist to begin
* with.</li>
* <li>There are both main and backup files. AAAAAaaa what do we do?! Throw an exception which will be reported.
* </li>
* </ol>
*
* @see StartableTransactionRunnable#onRollback()
*/
@Override
protected void onRollback()
{
// If this is false then we know run() has not yet happened and we know there is nothing to do.
if (this.preRunComplete) {
boolean isBackupFile = this.backupFile.exists();
boolean isMainFile = this.toDelete.exists();
// 1.
if (isBackupFile && !isMainFile) {
this.backupFile.renameTo(this.toDelete);
return;
}
// 2.
if (!isBackupFile && isMainFile) {
return;
}
// 3.
if (!isBackupFile && !isMainFile) {
return;
}
// 4.
if (isBackupFile && isMainFile) {
throw new IllegalStateException("Tried to rollback the deletion of file "
+ this.toDelete.getAbsolutePath() + " and encountered a "
+ "backup and a main file. Since the main file is renamed "
+ "to a backup location before deleting, this should never "
+ "happen.");
}
}
}
/**
* {@inheritDoc}
* <p>
* Once this is called, there is no going back.
* Remove backup file and unlock the lock.
* </p>
*
* @see StartableTransactionRunnable#onComplete()
*/
@Override
protected void onComplete() throws IOException
{
if (!this.preRunComplete) {
throw new IllegalStateException("Deleting file: " + this.toDelete.getAbsolutePath()
+ " onPreRun has not been called, maybe the class was extended "
+ "and it was overridden?");
}
try {
this.clearBackup();
} finally {
this.lock.writeLock().unlock();
}
}
/**
* Remove backup file.
*
* @throws IOException if removing file fails or file still exists after delete() is called.
*/
private void clearBackup() throws IOException
{
if (this.backupFile.exists()) {
this.backupFile.delete();
}
if (this.backupFile.exists()) {
throw new IOException("Could not remove backup file, cannot proceed.");
}
}
}