/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare 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 Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.storage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* That class handles the storage of a particular object to disk using Java serialisation.
* To support evolving versions of the serialised data we need to take care that we don't break the object structure.
* Java serialisation is tolerant with added fields, but removing or changing existing fields will break the backwards compatibility.
* Alternative frameworks for serialisation like Kyro or mapDB have shown problems with version migration, so we stuck with plain Java
* serialisation.
* <p>
* For every data object we write a separate file to minimize the risk of corrupted files in case of inconsistency from newer versions.
* In case of a corrupted file we backup the old file to a separate directory, so if it holds critical data it might be helpful for recovery.
* <p>
* We also backup at first read the file, so we have a valid file form the latest version in case a write operation corrupted the file.
* <p>
* The read operation is triggered just at object creation (startup) and is at the moment not executed on a background thread to avoid asynchronous behaviour.
* As the data are small and it is just one read access the performance penalty is small and might be even worse to create and setup a thread for it.
* <p>
* The write operation used a background thread and supports a delayed write to avoid too many repeated write operations.
*/
public class Storage<T extends Serializable> {
private static final Logger log = LoggerFactory.getLogger(Storage.class);
public static final String DIR_KEY = "storageDir";
private static DataBaseCorruptionHandler databaseCorruptionHandler;
public static void setDatabaseCorruptionHandler(DataBaseCorruptionHandler databaseCorruptionHandler) {
Storage.databaseCorruptionHandler = databaseCorruptionHandler;
}
public interface DataBaseCorruptionHandler {
void onFileCorrupted(String fileName);
}
private final File dir;
private FileManager<T> fileManager;
private File storageFile;
private T serializable;
private String fileName;
private int numMaxBackupFiles = 10;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public Storage(@Named(DIR_KEY) File dir) {
this.dir = dir;
}
public void initWithFileName(String fileName) {
this.fileName = fileName;
storageFile = new File(dir, fileName);
fileManager = new FileManager<>(dir, storageFile, 300);
}
@Nullable
public T initAndGetPersistedWithFileName(String fileName) {
this.fileName = fileName;
storageFile = new File(dir, fileName);
fileManager = new FileManager<>(dir, storageFile, 300);
return getPersisted();
}
@Nullable
public T initAndGetPersisted(T serializable) {
return initAndGetPersisted(serializable, serializable.getClass().getSimpleName());
}
@Nullable
public T initAndGetPersisted(T serializable, String fileName) {
this.serializable = serializable;
this.fileName = fileName;
storageFile = new File(dir, fileName);
fileManager = new FileManager<>(dir, storageFile, 600);
return getPersisted();
}
public void queueUpForSave() {
queueUpForSave(serializable);
}
public void queueUpForSave(long delayInMilli) {
queueUpForSave(serializable, delayInMilli);
}
public void setNumMaxBackupFiles(int numMaxBackupFiles) {
this.numMaxBackupFiles = numMaxBackupFiles;
}
// Save delayed and on a background thread
private void queueUpForSave(T serializable) {
if (serializable != null) {
log.trace("save " + fileName);
checkNotNull(storageFile, "storageFile = null. Call setupFileStorage before using read/write.");
fileManager.saveLater(serializable);
} else {
log.trace("queueUpForSave called but no serializable set");
}
}
public void queueUpForSave(T serializable, long delayInMilli) {
if (serializable != null) {
log.trace("save " + fileName);
checkNotNull(storageFile, "storageFile = null. Call setupFileStorage before using read/write.");
fileManager.saveLater(serializable, delayInMilli);
} else {
log.trace("queueUpForSave called but no serializable set");
}
}
public void remove(String fileName) {
fileManager.removeFile(fileName);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
// We do the file read on the UI thread to avoid problems from multi threading.
// Data are small and read is done only at startup, so it is no performance issue.
@Nullable
private T getPersisted() {
if (storageFile.exists()) {
long now = System.currentTimeMillis();
try {
T persistedObject = fileManager.read(storageFile);
log.trace("Read {} completed in {}msec", storageFile, System.currentTimeMillis() - now);
// If we did not get any exception we can be sure the data are consistent so we make a backup
now = System.currentTimeMillis();
fileManager.backupFile(fileName, numMaxBackupFiles);
log.trace("Backup {} completed in {}msec", storageFile, System.currentTimeMillis() - now);
return persistedObject;
} catch (Throwable t) {
log.error("Version of persisted class has changed. We cannot read the persisted data anymore. " +
"We make a backup and remove the inconsistent file.");
log.error(t.getMessage());
try {
// We keep a backup which might be used for recovery
fileManager.removeAndBackupFile(fileName);
} catch (IOException e1) {
e1.printStackTrace();
log.error(e1.getMessage());
// We swallow Exception if backup fails
}
if (databaseCorruptionHandler != null)
databaseCorruptionHandler.onFileCorrupted(storageFile.getName());
}
}
return null;
}
}