/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.database;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.syncany.database.ChunkEntry.ChunkChecksum;
import org.syncany.database.FileContent.FileChecksum;
import org.syncany.database.FileVersion.FileStatus;
import org.syncany.database.MultiChunkEntry.MultiChunkId;
import org.syncany.database.PartialFileHistory.FileHistoryId;
/**
* The database represents the internal file and chunk index of the application. It
* can be used to reference or load a full local database (local client) or a
* remote database (from a delta database file of another clients).
*
* <p>A database consists of a sorted list of {@link DatabaseVersion}s, i.e. it is a
* collection of changes to the local file system.
*
* <p>For convenience, the class also offers a set of functionality to select objects
* from the current accumulated database. Examples include {@link #getChunk(byte[]) getChunk()},
* {@link #getContent(byte[]) getContent()} and {@link #getMultiChunk(byte[]) getMultiChunk()}.
*
* <p>To allow this convenience, a few caches are kept in memory, and updated whenever a
* database version is added or removed.
*
* @see DatabaseVersion
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class MemoryDatabase {
private List<DatabaseVersion> databaseVersions;
// Caches
private DatabaseVersion fullDatabaseVersionCache;
private Map<String, PartialFileHistory> filenameHistoryCache;
private Map<VectorClock, DatabaseVersion> databaseVersionIdCache;
private Map<FileChecksum, List<PartialFileHistory>> contentChecksumFileHistoriesCache;
public MemoryDatabase() {
databaseVersions = new ArrayList<DatabaseVersion>();
// Caches
fullDatabaseVersionCache = new DatabaseVersion();
filenameHistoryCache = new HashMap<String, PartialFileHistory>();
databaseVersionIdCache = new HashMap<VectorClock, DatabaseVersion>();
contentChecksumFileHistoriesCache = new HashMap<FileChecksum, List<PartialFileHistory>>();
}
public DatabaseVersion getLastDatabaseVersion() {
if (databaseVersions.size() == 0) {
return null;
}
return databaseVersions.get(databaseVersions.size() - 1);
}
public List<DatabaseVersion> getDatabaseVersions() {
return Collections.unmodifiableList(databaseVersions);
}
public DatabaseVersion getDatabaseVersion(VectorClock vectorClock) {
return databaseVersionIdCache.get(vectorClock);
}
public FileContent getContent(FileChecksum checksum) {
return (checksum != null) ? fullDatabaseVersionCache.getFileContent(checksum) : null;
}
public Object getChunk(ChunkChecksum checksum) {
return (checksum != null) ? fullDatabaseVersionCache.getChunk(checksum) : null;
}
public MultiChunkEntry getMultiChunk(MultiChunkId id) {
return fullDatabaseVersionCache.getMultiChunk(id);
}
/**
* Get a multichunk that this chunk is contained in.
*/
public MultiChunkId getMultiChunkIdForChunk(ChunkChecksum chunk) {
return fullDatabaseVersionCache.getMultiChunkId(chunk);
}
public PartialFileHistory getFileHistory(String relativeFilePath) {
return filenameHistoryCache.get(relativeFilePath);
}
public List<PartialFileHistory> getFileHistories(FileChecksum fileContentChecksum) {
return contentChecksumFileHistoriesCache.get(fileContentChecksum);
}
public PartialFileHistory getFileHistory(FileHistoryId fileId) {
return fullDatabaseVersionCache.getFileHistory(fileId);
}
public Collection<PartialFileHistory> getFileHistories() {
return fullDatabaseVersionCache.getFileHistories();
}
public Collection<MultiChunkEntry> getMultiChunks() {
return fullDatabaseVersionCache.getMultiChunks();
}
public void addDatabaseVersion(DatabaseVersion databaseVersion) {
databaseVersions.add(databaseVersion);
// Populate caches
// WARNING: Do NOT reorder, order important!!
updateDatabaseVersionIdCache(databaseVersion);
updateFullDatabaseVersionCache(databaseVersion);
updateFilenameHistoryCache();
updateContentChecksumCache();
}
public void removeDatabaseVersion(DatabaseVersion databaseVersion) {
databaseVersions.remove(databaseVersion);
// Populate caches
// WARNING: Do NOT reorder, order important!!
updateFullDatabaseVersionCache();
updateDatabaseVersionIdCache();
updateFilenameHistoryCache();
updateContentChecksumCache();
}
// TODO [medium] Very inefficient. Always updates whole cache
private void updateContentChecksumCache() {
contentChecksumFileHistoriesCache.clear();
for (PartialFileHistory fullFileHistory : fullDatabaseVersionCache.getFileHistories()) {
FileChecksum lastVersionChecksum = fullFileHistory.getLastVersion().getChecksum();
if (lastVersionChecksum != null) {
List<PartialFileHistory> historiesWithVersionsWithSameChecksum = contentChecksumFileHistoriesCache.get(lastVersionChecksum);
// Create if it does not exist
if (historiesWithVersionsWithSameChecksum == null) {
historiesWithVersionsWithSameChecksum = new ArrayList<PartialFileHistory>();
}
// Add to cache
historiesWithVersionsWithSameChecksum.add(fullFileHistory);
contentChecksumFileHistoriesCache.put(lastVersionChecksum, historiesWithVersionsWithSameChecksum);
}
}
}
private void updateFilenameHistoryCache() {
// TODO [medium] Performance: This throws away the unchanged entries. It should only update new database version
filenameHistoryCache.clear();
for (PartialFileHistory cacheFileHistory : fullDatabaseVersionCache.getFileHistories()) {
FileVersion lastVersion = cacheFileHistory.getLastVersion();
String fileName = lastVersion.getPath();
if (lastVersion.getStatus() != FileStatus.DELETED) {
filenameHistoryCache.put(fileName, cacheFileHistory);
}
}
}
private void updateDatabaseVersionIdCache(DatabaseVersion newDatabaseVersion) {
databaseVersionIdCache.put(newDatabaseVersion.getVectorClock(), newDatabaseVersion);
}
private void updateDatabaseVersionIdCache() {
databaseVersionIdCache.clear();
for (DatabaseVersion databaseVersion : databaseVersions) {
updateDatabaseVersionIdCache(databaseVersion);
}
}
private void updateFullDatabaseVersionCache() {
fullDatabaseVersionCache = new DatabaseVersion();
for (DatabaseVersion databaseVersion : databaseVersions) {
updateFullDatabaseVersionCache(databaseVersion);
}
}
private void updateFullDatabaseVersionCache(DatabaseVersion newDatabaseVersion) {
// Chunks
for (ChunkEntry sourceChunk : newDatabaseVersion.getChunks()) {
if (fullDatabaseVersionCache.getChunk(sourceChunk.getChecksum()) == null) {
fullDatabaseVersionCache.addChunk(sourceChunk);
}
}
// Multichunks
for (MultiChunkEntry sourceMultiChunk : newDatabaseVersion.getMultiChunks()) {
if (fullDatabaseVersionCache.getMultiChunk(sourceMultiChunk.getId()) == null) {
fullDatabaseVersionCache.addMultiChunk(sourceMultiChunk);
}
}
// Contents
for (FileContent sourceFileContent : newDatabaseVersion.getFileContents()) {
if (fullDatabaseVersionCache.getFileContent(sourceFileContent.getChecksum()) == null) {
fullDatabaseVersionCache.addFileContent(sourceFileContent);
}
}
// Histories
for (PartialFileHistory sourceFileHistory : newDatabaseVersion.getFileHistories()) {
PartialFileHistory targetFileHistory = fullDatabaseVersionCache.getFileHistory(sourceFileHistory.getFileHistoryId());
if (targetFileHistory == null) {
fullDatabaseVersionCache.addFileHistory(sourceFileHistory.clone());
}
else {
for (FileVersion sourceFileVersion : sourceFileHistory.getFileVersions().values()) {
if (targetFileHistory.getFileVersion(sourceFileVersion.getVersion()) == null) {
targetFileHistory.addFileVersion(sourceFileVersion);
}
}
}
}
}
}