001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.database.dao; 019 020import java.sql.Connection; 021import java.sql.PreparedStatement; 022import java.sql.ResultSet; 023import java.sql.SQLException; 024import java.sql.Timestamp; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031 032import org.syncany.database.DatabaseVersion.DatabaseVersionStatus; 033import org.syncany.database.FileVersion; 034import org.syncany.database.PartialFileHistory; 035import org.syncany.database.PartialFileHistory.FileHistoryId; 036import org.syncany.database.VectorClock; 037 038import com.google.common.base.Function; 039import com.google.common.collect.Lists; 040 041/** 042 * The file history DAO queries and modifies the <i>filehistory</i> in 043 * the SQL database. This table corresponds to the Java object {@link PartialFileHistory}. 044 * 045 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 046 */ 047public class FileHistorySqlDao extends AbstractSqlDao { 048 private FileVersionSqlDao fileVersionDao; 049 050 public FileHistorySqlDao(Connection connection, FileVersionSqlDao fileVersionDao) { 051 super(connection); 052 this.fileVersionDao = fileVersionDao; 053 } 054 055 /** 056 * Writes a list of {@link PartialFileHistory}s to the database table <i>filehistory</i> using <code>INSERT</code>s 057 * and the given connection. In addition, this method also writes the corresponding {@link FileVersion}s of 058 * each file history to the database using 059 * {@link FileVersionSqlDao#writeFileVersions(Connection, FileHistoryId, long, Collection) FileVersionSqlDao#writeFileVersions}. 060 * 061 * <p><b>Note:</b> This method executes, but <b>does not commit</b> the queries. 062 * 063 * @param connection The connection used to execute the statements 064 * @param databaseVersionId References the {@link PartialFileHistory} to which the list of file versions belongs 065 * @param fileHistories List of {@link PartialFileHistory}s to be written to the database 066 * @throws SQLException If the SQL statement fails 067 */ 068 public void writeFileHistories(Connection connection, long databaseVersionId, Collection<PartialFileHistory> fileHistories) throws SQLException { 069 for (PartialFileHistory fileHistory : fileHistories) { 070 PreparedStatement preparedStatement = getStatement(connection, "filehistory.insert.all.writeFileHistories.sql"); 071 072 preparedStatement.setString(1, fileHistory.getFileHistoryId().toString()); 073 preparedStatement.setLong(2, databaseVersionId); 074 075 int affectedRows = preparedStatement.executeUpdate(); 076 077 if (affectedRows == 0) { 078 throw new SQLException("Cannot add database version header. Affected rows is zero."); 079 } 080 081 preparedStatement.close(); 082 083 fileVersionDao.writeFileVersions(connection, fileHistory.getFileHistoryId(), databaseVersionId, fileHistory.getFileVersions().values()); 084 } 085 } 086 087 public void removeDirtyFileHistories() throws SQLException { 088 try (PreparedStatement preparedStatement = getStatement("filehistory.delete.dirty.removeDirtyFileHistories.sql")) { 089 preparedStatement.executeUpdate(); 090 } 091 } 092 093 /** 094 * Removes unreferenced {@link PartialFileHistory}s from the database table 095 * <i>filehistory</i>. This method <b>does not</b> remove the corresponding {@link FileVersion}s. 096 * 097 * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query. 098 * 099 * @throws SQLException If the SQL statement fails 100 */ 101 public void removeUnreferencedFileHistories() throws SQLException { 102 try (PreparedStatement preparedStatement = getStatement("filehistory.delete.all.removeUnreferencedFileHistories.sql")) { 103 preparedStatement.executeUpdate(); 104 } 105 } 106 107 /** 108 * Note: Also selects versions marked as {@link DatabaseVersionStatus#DIRTY DIRTY} 109 */ 110 public Map<FileHistoryId, PartialFileHistory> getFileHistoriesWithFileVersions(VectorClock databaseVersionVectorClock, int maxCount) { 111 try (PreparedStatement preparedStatement = getStatement("filehistory.select.all.getFileHistoriesWithFileVersionsByVectorClock.sql")) { 112 preparedStatement.setString(1, databaseVersionVectorClock.toString()); 113 114 if (maxCount > 0) { 115 preparedStatement.setMaxRows(maxCount); 116 } 117 118 try (ResultSet resultSet = preparedStatement.executeQuery()) { 119 return createFileHistoriesFromResult(resultSet); 120 } 121 } 122 catch (SQLException e) { 123 throw new RuntimeException(e); 124 } 125 } 126 127 public FileHistoryId expandFileHistoryId(FileHistoryId fileHistoryIdPrefix) { 128 String fileHistoryIdPrefixLikeQuery = fileHistoryIdPrefix.toString() + "%"; 129 130 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.expandFileHistoryId.sql")) { 131 preparedStatement.setString(1, fileHistoryIdPrefixLikeQuery); 132 133 try (ResultSet resultSet = preparedStatement.executeQuery()) { 134 if (resultSet.next()) { 135 FileHistoryId fullFileHistoryId = FileHistoryId.parseFileId(resultSet.getString("filehistory_id")); 136 137 boolean nonUniqueResult = resultSet.next(); 138 139 if (nonUniqueResult) { 140 return null; 141 } 142 else { 143 return fullFileHistoryId; 144 } 145 } 146 else { 147 return null; 148 } 149 } 150 } 151 catch (SQLException e) { 152 throw new RuntimeException(e); 153 } 154 } 155 156 public Map<FileHistoryId, PartialFileHistory> getFileHistories(List<FileHistoryId> fileHistoryIds) { 157 String[] fileHistoryIdsStr = createFileHistoryIdsArray(fileHistoryIds); 158 159 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesByIds.sql")) { 160 preparedStatement.setArray(1, connection.createArrayOf("varchar", fileHistoryIdsStr)); 161 162 try (ResultSet resultSet = preparedStatement.executeQuery()) { 163 return createFileHistoriesFromResult(resultSet); 164 } 165 } 166 catch (SQLException e) { 167 throw new RuntimeException(e); 168 } 169 } 170 171 /** 172 * This function returns FileHistories with the last version for which this last version 173 * matches the given checksum, size and modified date. 174 * 175 * @return An empty Collection is returned if none exist. 176 */ 177 public Collection<PartialFileHistory> getFileHistoriesByChecksumSizeAndModifiedDate(String filecontentChecksum, long size, Date modifiedDate) { 178 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesByChecksumSizeAndModifiedDate.sql")) { 179 // This first query retrieves the last version for each FileHistory matching the three requested properties. 180 // However, it does not guarantee that this version is indeed the last version in that particular 181 // FileHistory, so we need another query to verify that. 182 183 preparedStatement.setString(1, filecontentChecksum); 184 preparedStatement.setLong(2, size); 185 preparedStatement.setTimestamp(3, new Timestamp(modifiedDate.getTime())); 186 187 try (ResultSet resultSet = preparedStatement.executeQuery()) { 188 Collection<PartialFileHistory> fileHistories = new ArrayList<>(); 189 190 while (resultSet.next()) { 191 String fileHistoryId = resultSet.getString("filehistory_id"); 192 PartialFileHistory fileHistory = getLastVersionByFileHistoryId(fileHistoryId); 193 194 boolean resultIsLatestVersion = fileHistory.getLastVersion().getVersion() == resultSet.getLong("version"); 195 boolean resultIsNotDelete = fileHistory.getLastVersion().getStatus() != FileVersion.FileStatus.DELETED; 196 197 // Only if the result is indeed the last in it's history, we can use it 198 // to base other versions off it. So we return it. 199 200 if (resultIsLatestVersion && resultIsNotDelete) { 201 fileHistories.add(fileHistory); 202 } 203 } 204 205 return fileHistories; 206 } 207 208 } 209 catch (SQLException e) { 210 throw new RuntimeException(e); 211 } 212 } 213 214 /** 215 * This function returns a FileHistory, with as last version a FileVersion with 216 * the given path. 217 * 218 * If the last FileVersion referring to this path is not the last in the 219 * FileHistory, or no such FileVersion exists, null is returned. 220 */ 221 public PartialFileHistory getFileHistoryWithLastVersionByPath(String path) { 222 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.findLatestFileVersionsForPath.sql")) { 223 preparedStatement.setString(1, path); 224 225 try (ResultSet resultSet = preparedStatement.executeQuery()) { 226 // Fetch the latest versions of all files that once existed with the given 227 // path and find the most recent by comparing vector clocks 228 229 String latestFileHistoryId = null; 230 Long latestFileVersion = null; 231 VectorClock latestVectorClock = null; 232 233 while (resultSet.next()) { 234 VectorClock resultSetVectorClock = VectorClock.parseVectorClock(resultSet.getString("vectorclock_serialized")); 235 boolean vectorClockIsGreater = latestVectorClock == null 236 || VectorClock.compare(resultSetVectorClock, latestVectorClock) == VectorClock.VectorClockComparison.GREATER; 237 238 if (vectorClockIsGreater) { 239 latestVectorClock = resultSetVectorClock; 240 latestFileHistoryId = resultSet.getString("filehistory_id"); 241 latestFileVersion = resultSet.getLong("version"); 242 } 243 } 244 245 // If no active file history exists for this path, return 246 if (latestFileHistoryId == null) { 247 return null; 248 } 249 250 // Get the last FileVersion of the FileHistory in the database with the largest vectorclock. 251 PartialFileHistory fileHistory = getLastVersionByFileHistoryId(latestFileHistoryId); 252 253 // The above query does not guarantee the resulting version is the last in its 254 // history. We need to check this before returning the file. 255 if (fileHistory.getLastVersion().getVersion() == latestFileVersion) { 256 return fileHistory; 257 } 258 else { 259 // The version retrieved by the path query is not a fileversion which is in the current 260 // filetree. Since it was the last version with this path, there is no other history 261 // which should be continued. 262 return null; 263 } 264 } 265 } 266 catch (SQLException e) { 267 throw new RuntimeException(e); 268 } 269 } 270 271 private PartialFileHistory getLastVersionByFileHistoryId(String fileHistoryId) { 272 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getLastVersionByFileHistoryId.sql")) { 273 preparedStatement.setString(1, fileHistoryId); 274 preparedStatement.setString(2, fileHistoryId); 275 276 try (ResultSet resultSet = preparedStatement.executeQuery()) { 277 if (resultSet.next()) { 278 FileVersion lastFileVersion = fileVersionDao.createFileVersionFromRow(resultSet); 279 FileHistoryId fileHistoryIdData = FileHistoryId.parseFileId(resultSet.getString("filehistory_id")); 280 281 PartialFileHistory fileHistory = new PartialFileHistory(fileHistoryIdData); 282 fileHistory.addFileVersion(lastFileVersion); 283 284 return fileHistory; 285 } 286 else { 287 return null; 288 } 289 } 290 } 291 catch (SQLException e) { 292 throw new RuntimeException(e); 293 } 294 } 295 296 private String[] createFileHistoryIdsArray(List<FileHistoryId> fileHistoryIds) { 297 return Lists.transform(fileHistoryIds, new Function<FileHistoryId, String>() { 298 @Override 299 public String apply(FileHistoryId fileHistoryId) { 300 return fileHistoryId.toString(); 301 } 302 }).toArray(new String[0]); 303 } 304 305 public Map<FileHistoryId, PartialFileHistory> getFileHistoriesWithFileVersions() { 306 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesWithFileVersions.sql")) { 307 try (ResultSet resultSet = preparedStatement.executeQuery()) { 308 return createFileHistoriesFromResult(resultSet); 309 } 310 } 311 catch (SQLException e) { 312 throw new RuntimeException(e); 313 } 314 } 315 316 protected Map<FileHistoryId, PartialFileHistory> createFileHistoriesFromResult(ResultSet resultSet) throws SQLException { 317 Map<FileHistoryId, PartialFileHistory> fileHistories = new HashMap<FileHistoryId, PartialFileHistory>(); 318 PartialFileHistory fileHistory = null; 319 320 while (resultSet.next()) { 321 FileVersion lastFileVersion = fileVersionDao.createFileVersionFromRow(resultSet); 322 FileHistoryId fileHistoryId = FileHistoryId.parseFileId(resultSet.getString("filehistory_id")); 323 324 // Old history (= same filehistory identifier) 325 if (fileHistory != null && fileHistory.getFileHistoryId().equals(fileHistoryId)) { // Same history! 326 fileHistory.addFileVersion(lastFileVersion); 327 } 328 329 // New history! 330 else { 331 // Add the old history 332 if (fileHistory != null) { 333 fileHistories.put(fileHistory.getFileHistoryId(), fileHistory); 334 } 335 336 // Create a new one 337 fileHistory = new PartialFileHistory(fileHistoryId); 338 fileHistory.addFileVersion(lastFileVersion); 339 } 340 } 341 342 // Add the last history 343 if (fileHistory != null) { 344 fileHistories.put(fileHistory.getFileHistoryId(), fileHistory); 345 } 346 347 return fileHistories; 348 } 349 350 public List<PartialFileHistory> getFileHistoriesWithLastVersion() { 351 List<PartialFileHistory> fileHistories = new ArrayList<PartialFileHistory>(); 352 353 try (PreparedStatement preparedStatement = getStatement("filehistory.select.master.getFileHistoriesWithLastVersion.sql")) { 354 try (ResultSet resultSet = preparedStatement.executeQuery()) { 355 while (resultSet.next()) { 356 FileHistoryId fileHistoryId = FileHistoryId.parseFileId(resultSet.getString("filehistory_id")); 357 FileVersion lastFileVersion = fileVersionDao.createFileVersionFromRow(resultSet); 358 359 PartialFileHistory fileHistory = new PartialFileHistory(fileHistoryId); 360 fileHistory.addFileVersion(lastFileVersion); 361 362 fileHistories.add(fileHistory); 363 } 364 } 365 366 return fileHistories; 367 } 368 catch (SQLException e) { 369 throw new RuntimeException(e); 370 } 371 } 372}