/*
* 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.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.syncany.database.DatabaseVersion.DatabaseVersionStatus;
import org.syncany.database.FileVersion;
import org.syncany.database.PartialFileHistory;
import org.syncany.database.PartialFileHistory.FileHistoryId;
import org.syncany.database.VectorClock;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
/**
* The file history DAO queries and modifies the <i>filehistory</i> in
* the SQL database. This table corresponds to the Java object {@link PartialFileHistory}.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class FileHistorySqlDao extends AbstractSqlDao {
private FileVersionSqlDao fileVersionDao;
public FileHistorySqlDao(Connection connection, FileVersionSqlDao fileVersionDao) {
super(connection);
this.fileVersionDao = fileVersionDao;
}
/**
* Writes a list of {@link PartialFileHistory}s to the database table <i>filehistory</i> using <tt>INSERT</tt>s
* and the given connection. In addition, this method also writes the corresponding {@link FileVersion}s of
* each file history to the database using
* {@link FileVersionSqlDao#writeFileVersions(Connection, FileHistoryId, long, Collection) FileVersionSqlDao#writeFileVersions}.
*
* <p><b>Note:</b> This method executes, but <b>does not commit</b> the queries.
*
* @param connection The connection used to execute the statements
* @param databaseVersionId References the {@link PartialFileHistory} to which the list of file versions belongs
* @param fileHistories List of {@link PartialFileHistory}s to be written to the database
* @throws SQLException If the SQL statement fails
*/
public void writeFileHistories(Connection connection, long databaseVersionId, Collection<PartialFileHistory> fileHistories) throws SQLException {
for (PartialFileHistory fileHistory : fileHistories) {
PreparedStatement preparedStatement = getStatement(connection, "filehistory.insert.all.writeFileHistories.sql");
preparedStatement.setString(1, fileHistory.getFileHistoryId().toString());
preparedStatement.setLong(2, databaseVersionId);
int affectedRows = preparedStatement.executeUpdate();
if (affectedRows == 0) {
throw new SQLException("Cannot add database version header. Affected rows is zero.");
}
preparedStatement.close();
fileVersionDao.writeFileVersions(connection, fileHistory.getFileHistoryId(), databaseVersionId, fileHistory.getFileVersions().values());
}
}
public void removeDirtyFileHistories() throws SQLException {
try (PreparedStatement preparedStatement = getStatement("filehistory.delete.dirty.removeDirtyFileHistories.sql")) {
preparedStatement.executeUpdate();
}
}
/**
* Removes unreferenced {@link PartialFileHistory}s from the database table
* <i>filehistory</i>. This method <b>does not</b> remove the corresponding {@link FileVersion}s.
*
* <p><b>Note:</b> This method executes, but <b>does not commit</b> the query.
*
* @throws SQLException If the SQL statement fails
*/
public void removeUnreferencedFileHistories() throws SQLException {
try (PreparedStatement preparedStatement = getStatement("filehistory.delete.all.removeUnreferencedFileHistories.sql")) {
preparedStatement.executeUpdate();
}
}
/**
* Note: Also selects versions marked as {@link DatabaseVersionStatus#DIRTY DIRTY}
*/
public Map<FileHistoryId, PartialFileHistory> getFileHistoriesWithFileVersions(VectorClock databaseVersionVectorClock, int maxCount) {
try (PreparedStatement preparedStatement = getStatement("filehistory.select.all.getFileHistoriesWithFileVersionsByVectorClock.sql")) {
preparedStatement.setString(1, databaseVersionVectorClock.toString());
if (maxCount > 0) {
preparedStatement.setMaxRows(maxCount);
}
try (ResultSet resultSet = preparedStatement.executeQuery()) {
return createFileHistoriesFromResult(resultSet);
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
public FileHistoryId expandFileHistoryId(FileHistoryId fileHistoryIdPrefix) {
String fileHistoryIdPrefixLikeQuery = fileHistoryIdPrefix.toString() + "%";
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.expandFileHistoryId.sql")) {
preparedStatement.setString(1, fileHistoryIdPrefixLikeQuery);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
FileHistoryId fullFileHistoryId = FileHistoryId.parseFileId(resultSet.getString("filehistory_id"));
boolean nonUniqueResult = resultSet.next();
if (nonUniqueResult) {
return null;
}
else {
return fullFileHistoryId;
}
}
else {
return null;
}
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Map<FileHistoryId, PartialFileHistory> getFileHistories(List<FileHistoryId> fileHistoryIds) {
String[] fileHistoryIdsStr = createFileHistoryIdsArray(fileHistoryIds);
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesByIds.sql")) {
preparedStatement.setArray(1, connection.createArrayOf("varchar", fileHistoryIdsStr));
try (ResultSet resultSet = preparedStatement.executeQuery()) {
return createFileHistoriesFromResult(resultSet);
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* This function returns FileHistories with the last version for which this last version
* matches the given checksum, size and modified date.
*
* @return An empty Collection is returned if none exist.
*/
public Collection<PartialFileHistory> getFileHistoriesByChecksumSizeAndModifiedDate(String filecontentChecksum, long size, Date modifiedDate) {
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesByChecksumSizeAndModifiedDate.sql")) {
// This first query retrieves the last version for each FileHistory matching the three requested properties.
// However, it does not guarantee that this version is indeed the last version in that particular
// FileHistory, so we need another query to verify that.
preparedStatement.setString(1, filecontentChecksum);
preparedStatement.setLong(2, size);
preparedStatement.setTimestamp(3, new Timestamp(modifiedDate.getTime()));
try (ResultSet resultSet = preparedStatement.executeQuery()) {
Collection<PartialFileHistory> fileHistories = new ArrayList<>();
while (resultSet.next()) {
String fileHistoryId = resultSet.getString("filehistory_id");
PartialFileHistory fileHistory = getLastVersionByFileHistoryId(fileHistoryId);
boolean resultIsLatestVersion = fileHistory.getLastVersion().getVersion() == resultSet.getLong("version");
boolean resultIsNotDelete = fileHistory.getLastVersion().getStatus() != FileVersion.FileStatus.DELETED;
// Only if the result is indeed the last in it's history, we can use it
// to base other versions off it. So we return it.
if (resultIsLatestVersion && resultIsNotDelete) {
fileHistories.add(fileHistory);
}
}
return fileHistories;
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* This function returns a FileHistory, with as last version a FileVersion with
* the given path.
*
* If the last FileVersion referring to this path is not the last in the
* FileHistory, or no such FileVersion exists, null is returned.
*/
public PartialFileHistory getFileHistoryWithLastVersionByPath(String path) {
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.findLatestFileVersionsForPath.sql")) {
preparedStatement.setString(1, path);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
// Fetch the latest versions of all files that once existed with the given
// path and find the most recent by comparing vector clocks
String latestFileHistoryId = null;
Long latestFileVersion = null;
VectorClock latestVectorClock = null;
while (resultSet.next()) {
VectorClock resultSetVectorClock = VectorClock.parseVectorClock(resultSet.getString("vectorclock_serialized"));
boolean vectorClockIsGreater = latestVectorClock == null
|| VectorClock.compare(resultSetVectorClock, latestVectorClock) == VectorClock.VectorClockComparison.GREATER;
if (vectorClockIsGreater) {
latestVectorClock = resultSetVectorClock;
latestFileHistoryId = resultSet.getString("filehistory_id");
latestFileVersion = resultSet.getLong("version");
}
}
// If no active file history exists for this path, return
if (latestFileHistoryId == null) {
return null;
}
// Get the last FileVersion of the FileHistory in the database with the largest vectorclock.
PartialFileHistory fileHistory = getLastVersionByFileHistoryId(latestFileHistoryId);
// The above query does not guarantee the resulting version is the last in its
// history. We need to check this before returning the file.
if (fileHistory.getLastVersion().getVersion() == latestFileVersion) {
return fileHistory;
}
else {
// The version retrieved by the path query is not a fileversion which is in the current
// filetree. Since it was the last version with this path, there is no other history
// which should be continued.
return null;
}
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
private PartialFileHistory getLastVersionByFileHistoryId(String fileHistoryId) {
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getLastVersionByFileHistoryId.sql")) {
preparedStatement.setString(1, fileHistoryId);
preparedStatement.setString(2, fileHistoryId);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
if (resultSet.next()) {
FileVersion lastFileVersion = fileVersionDao.createFileVersionFromRow(resultSet);
FileHistoryId fileHistoryIdData = FileHistoryId.parseFileId(resultSet.getString("filehistory_id"));
PartialFileHistory fileHistory = new PartialFileHistory(fileHistoryIdData);
fileHistory.addFileVersion(lastFileVersion);
return fileHistory;
}
else {
return null;
}
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String[] createFileHistoryIdsArray(List<FileHistoryId> fileHistoryIds) {
return Lists.transform(fileHistoryIds, new Function<FileHistoryId, String>() {
@Override
public String apply(FileHistoryId fileHistoryId) {
return fileHistoryId.toString();
}
}).toArray(new String[0]);
}
public Map<FileHistoryId, PartialFileHistory> getFileHistoriesWithFileVersions() {
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesWithFileVersions.sql")) {
try (ResultSet resultSet = preparedStatement.executeQuery()) {
return createFileHistoriesFromResult(resultSet);
}
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
protected Map<FileHistoryId, PartialFileHistory> createFileHistoriesFromResult(ResultSet resultSet) throws SQLException {
Map<FileHistoryId, PartialFileHistory> fileHistories = new HashMap<FileHistoryId, PartialFileHistory>();
PartialFileHistory fileHistory = null;
while (resultSet.next()) {
FileVersion lastFileVersion = fileVersionDao.createFileVersionFromRow(resultSet);
FileHistoryId fileHistoryId = FileHistoryId.parseFileId(resultSet.getString("filehistory_id"));
// Old history (= same filehistory identifier)
if (fileHistory != null && fileHistory.getFileHistoryId().equals(fileHistoryId)) { // Same history!
fileHistory.addFileVersion(lastFileVersion);
}
// New history!
else {
// Add the old history
if (fileHistory != null) {
fileHistories.put(fileHistory.getFileHistoryId(), fileHistory);
}
// Create a new one
fileHistory = new PartialFileHistory(fileHistoryId);
fileHistory.addFileVersion(lastFileVersion);
}
}
// Add the last history
if (fileHistory != null) {
fileHistories.put(fileHistory.getFileHistoryId(), fileHistory);
}
return fileHistories;
}
public List<PartialFileHistory> getFileHistoriesWithLastVersion() {
List<PartialFileHistory> fileHistories = new ArrayList<PartialFileHistory>();
try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesWithLastVersion.sql")) {
try (ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
FileHistoryId fileHistoryId = FileHistoryId.parseFileId(resultSet.getString("filehistory_id"));
FileVersion lastFileVersion = fileVersionDao.createFileVersionFromRow(resultSet);
PartialFileHistory fileHistory = new PartialFileHistory(fileHistoryId);
fileHistory.addFileVersion(lastFileVersion);
fileHistories.add(fileHistory);
}
}
return fileHistories;
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
}