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}