/*
* 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 saving 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: 961e9444da98909ae9cec7d19db88904d18a33d6 $
* @since 3.0M2
*/
public class FileSaveTransactionRunnable extends StartableTransactionRunnable<TransactionRunnable>
{
/**
* The location of the file to save the attachment content in.
*/
private final File toSave;
/**
* The location of the temporary file.
*/
private final File tempFile;
/**
* The location of the backup file.
*/
private final File backupFile;
/**
* A lock to hold while running this TransactionRunnable.
*/
private final ReadWriteLock lock;
/**
* The serializer.
*/
private final FileSerializer serializer;
/**
* False until run() 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) save operation.
*/
private boolean runComplete;
/**
* The Constructor.
*
* @param toSave the file to put the content in.
* @param tempFile 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 the data
* until onCommit when it is renamed to the toSave file.
* @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 toSave 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.
* @param provider a StreamProvider to get the data to put into the file.
*/
public FileSaveTransactionRunnable(final File toSave,
final File tempFile,
final File backupFile,
final ReadWriteLock lock,
final StreamProvider provider)
{
this.toSave = toSave;
this.tempFile = tempFile;
this.backupFile = backupFile;
this.lock = lock;
this.serializer = new StreamProviderFileSerializer(provider);
}
/**
* The Constructor.
*
* @param toSave the file to put the content in.
* @param tempFile 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 the data
* until onCommit when it is renamed to the toSave file.
* @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 toSave 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.
* @param serializer a FileSerializer in charge to serializing what need to be serialize to the file.
* @since 9.0RC1
*/
public FileSaveTransactionRunnable(final File toSave,
final File tempFile,
final File backupFile,
final ReadWriteLock lock,
final FileSerializer serializer)
{
this.toSave = toSave;
this.tempFile = tempFile;
this.backupFile = backupFile;
this.lock = lock;
this.serializer = serializer;
}
/**
* {@inheritDoc}
* <p>
* Obtain the lock and make sure the temporary and backup files are deleted.
* </p>
*
* @see TransactionRunnable#preRun()
*/
@Override
protected void onPreRun() throws IOException
{
this.lock.writeLock().lock();
this.clearTempAndBackup();
}
/**
* {@inheritDoc}
* <p>
* Write the data from the provider to the temporary file.
* </p>
*
* @see TransactionRunnable#run()
*/
@Override
protected void onRun() throws Exception
{
if (!this.toSave.getParentFile().exists() && !this.toSave.getParentFile().mkdirs()) {
throw new IOException("Could not make directory tree to place file in. "
+ "Do you have permission to write to ["
+ this.toSave.getAbsolutePath() + "] ?");
}
try {
this.serializer.serialize(this.tempFile);
} finally {
this.runComplete = true;
}
}
/**
* {@inheritDoc}
* <p>
* Move whatever is in the main file location into backup and move
* the temp file into the main location.
* </p>
*
* @see TransactionRunnable#onCommit()
*/
@Override
protected void onCommit()
{
if (this.toSave.exists()) {
this.toSave.renameTo(this.backupFile);
}
this.tempFile.renameTo(this.toSave);
}
@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.runComplete) {
if (this.tempFile.exists()) {
this.onRollbackWithTempFile();
} else {
this.onRollbackNoTempFile();
}
}
}
/**
* {@inheritDoc}
* <p>
* Once this is called, there is no going back.
* Remove temporary and backup files and unlock the lock.
* </p>
*
* @see TransactionRunnable#onComplete()
*/
@Override
protected void onComplete() throws IOException
{
try {
this.clearTempAndBackup();
} finally {
this.lock.writeLock().unlock();
}
}
/**
* Knowing there is no temp file, we can determine that one of 4 possible things happened.
*
* 1. No backup file and no main file. There was probably no file to begin with
* and it failed before anything could be saved in the temp file. Do nothing.
*
* 2. No backup file but there is a main file, assume onCommit happened successfully but there
* was no file here to begin with so there was nothing to back up. Move the main file back
* to the temporary location.
*
* 3. If there is a backup file, but no main file, this is unexpected but since the backup file
* should be the previous main file, move it back to the main location and log a warning
* that the storage engine encountered an unexpected albeit probably recoverable state.
*
* 4. If there is a backup file and a main file, onCommit probably went smoothly and a problem
* was encountered somewhere else forcing the rollback. Move the main file back to the
* temporary location and the backup file back to the main location.
*/
private void onRollbackNoTempFile()
{
boolean isBackupFile = this.backupFile.exists();
boolean isMainFile = this.toSave.exists();
// 1.
if (!isBackupFile && !isMainFile) {
return;
}
// 2.
if (!isBackupFile && isMainFile) {
this.toSave.renameTo(this.tempFile);
return;
}
// 3.
if (isBackupFile && !isMainFile) {
this.backupFile.renameTo(this.toSave);
// TODO log a low severity warning.
return;
}
// 4.
if (isBackupFile && isMainFile) {
this.toSave.renameTo(this.tempFile);
this.backupFile.renameTo(this.toSave);
return;
}
}
/**
* Knowing there is a temp file, one of 3 things might have happened:
*
* 1. If there is no backup file, assume onCommit did not occur, do nothing regardless
* of whether there is or isn't an (existing) main file.
*
* 2. If there is a backup file but no main file, there must have been a failure half way
* through onCommit, it was able to move the existing main file to the backup
* location but did not move the temporary file to the main location. Move the backup file
* back to the main location.
*
* 3. If there is a file in each location which should not happen and if it does,
* throw an exception which will be printed in the log.
*/
private void onRollbackWithTempFile()
{
boolean isBackupFile = this.backupFile.exists();
boolean isMainFile = this.toSave.exists();
// 1.
if (!isBackupFile) {
return;
}
// 2.
if (isBackupFile && !isMainFile) {
this.backupFile.renameTo(this.toSave);
return;
}
// 3.
if (isBackupFile && isMainFile) {
throw new IllegalStateException("Tried to rollback the saving of file "
+ this.toSave.getAbsolutePath() + " and encountered a "
+ "backup, a temp file, and a main file. Since any existing "
+ "main file is renamed to a temp location and the content is "
+ "saved in the backup location and then renamed to the main "
+ "location, the existance of all 3 at once should never "
+ "happen.");
}
}
/**
* Remove temporary and backup files.
*
* @throws IOException if removing files fails or files still exist after delete() is called.
*/
private void clearTempAndBackup() throws IOException
{
if (this.tempFile.exists()) {
this.tempFile.delete();
}
if (this.tempFile.exists()) {
throw new IOException("Could not remove temporary file, cannot proceed.");
}
if (this.backupFile.exists()) {
this.backupFile.delete();
}
if (this.backupFile.exists()) {
throw new IOException("Could not remove backup file, cannot proceed.");
}
}
}