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.util.Collection;
025import java.util.HashMap;
026import java.util.Map;
027
028import org.syncany.database.ChunkEntry.ChunkChecksum;
029import org.syncany.database.FileContent;
030import org.syncany.database.FileContent.FileChecksum;
031import org.syncany.database.VectorClock;
032
033/**
034 * The file content data access object (DAO) writes and queries the SQL database for information
035 * on {@link FileContent}s. It translates the relational data in the <i>filecontent</i> table to
036 * Java objects.
037 * 
038 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
039 */
040public class FileContentSqlDao extends AbstractSqlDao {
041        public FileContentSqlDao(Connection connection) {
042                super(connection);
043        }
044
045        /**
046         * Writes a list of {@link FileContent}s to the database using <code>INSERT</code>s and the given connection.
047         * It fills two tables, the <i>filecontent</i> table ({@link FileContent}) and the <i>filecontent_chunk</i> 
048         * table ({@link ChunkChecksum}).
049         * 
050         * <p>To do the latter (write chunk references), this method calls
051         * {@link #writeFileContentChunkRefs(Connection, FileContent) writeFileContentChunkRefs()} for every 
052         * {@link FileContent}. 
053         * 
054         * <p><b>Note:</b> This method executes, but does not commit the queries.
055         * 
056         * @param connection The connection used to execute the statements
057         * @param databaseVersionId 
058         * @param fileContents List of {@link FileContent}s to be inserted in the database
059         * @throws SQLException If the SQL statement fails
060         */
061        public void writeFileContents(Connection connection, long databaseVersionId, Collection<FileContent> fileContents) throws SQLException {
062                for (FileContent fileContent : fileContents) {
063                        PreparedStatement preparedStatement = getStatement(connection, "filecontent.insert.all.writeFileContents.sql");
064
065                        preparedStatement.setString(1, fileContent.getChecksum().toString());
066                        preparedStatement.setLong(2, databaseVersionId);
067                        preparedStatement.setLong(3, fileContent.getSize());
068                        
069                        preparedStatement.executeUpdate();
070                        preparedStatement.close();      
071                        
072                        // Write chunk references
073                        writeFileContentChunkRefs(connection, fileContent);                     
074                }
075        }
076        
077        private void writeFileContentChunkRefs(Connection connection, FileContent fileContent) throws SQLException {
078                PreparedStatement preparedStatement = getStatement(connection, "filecontent.insert.all.writeFileContentChunkRefs.sql");
079                int order = 0;
080                
081                for (ChunkChecksum chunkChecksum : fileContent.getChunks()) {
082                        preparedStatement.setString(1, fileContent.getChecksum().toString());
083                        preparedStatement.setString(2, chunkChecksum.toString());
084                        preparedStatement.setInt(3, order);
085
086                        preparedStatement.addBatch();
087                        
088                        order++;                                
089                }
090                
091                preparedStatement.executeBatch();
092                preparedStatement.close();
093        }
094
095        /**
096         * Removes unreferenced {@link FileContent}s from the database table <i>filecontent</i>,
097         * as well as the corresponding chunk references (list of {@link ChunkChecksum}s) from the
098         * table <i>filecontent_chunk</i>. 
099         * 
100         * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query.
101         * 
102         * @throws SQLException If the SQL statement fails
103         */
104        public void removeUnreferencedFileContents() throws SQLException {
105                // Note: Chunk references (filcontent_chunk) must be removed first, because
106                //       of the foreign key constraints. 
107                
108                removeUnreferencedFileContentChunkRefs();
109                removeUnreferencedFileContentsInt();
110        }
111        
112        private void removeUnreferencedFileContentsInt() throws SQLException {
113                PreparedStatement preparedStatement = getStatement("filecontent.delete.all.removeUnreferencedFileContents.sql");
114                preparedStatement.executeUpdate();      
115                preparedStatement.close();
116        }
117        
118        private void removeUnreferencedFileContentChunkRefs() throws SQLException {
119                PreparedStatement preparedStatement = getStatement("filecontent.delete.all.removeUnreferencedFileContentRefs.sql");
120                preparedStatement.executeUpdate();      
121                preparedStatement.close();
122        }
123        
124        /**
125         * Queries the database for a particular {@link FileContent}, either with or without the
126         * corresponding chunk references (list of {@link ChunkChecksum}). 
127         * 
128         * @param fileChecksum {@link FileContent}-identifying file checksum
129         * @param includeChunkChecksums If <code>true</code>, the resulting {@link FileContent} will contain its chunk references
130         * @return Returns a {@link FileContent} either with or without chunk references, or <code>null</code> if it does not exist.
131         */
132        public FileContent getFileContent(FileChecksum fileChecksum, boolean includeChunkChecksums) {
133                if (fileChecksum == null) {
134                        return null;
135                }
136                else if (includeChunkChecksums) {
137                        return getFileContentWithChunkChecksums(fileChecksum);                  
138                }
139                else {
140                        return getFileContentWithoutChunkChecksums(fileChecksum);                       
141                }
142        }
143
144        /**
145         * Queries the SQL database for all {@link FileContent}s that <b>originally appeared</b> in the
146         * database version identified by the given vector clock.
147         * 
148         * <p><b>Note:</b> This method does <b>not</b> select all the file contents that are referenced
149         * in the database version. In particular, it <b>does not return</b> file contents that appeared
150         * in previous other database versions.
151         * 
152         * @param vectorClock Vector clock that identifies the database version
153         * @return Returns all {@link FileContent}s that originally belong to a database version
154         */
155        public Map<FileChecksum, FileContent> getFileContents(VectorClock vectorClock) {
156                try (PreparedStatement preparedStatement = getStatement("filecontent.select.master.getFileContentsWithChunkChecksumsForDatabaseVersion.sql")) {
157                        preparedStatement.setString(1, vectorClock.toString());
158
159                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
160                                return createFileContents(resultSet);
161                        }
162                }
163                catch (SQLException e) {
164                        throw new RuntimeException(e);
165                }
166        }
167
168        private FileContent getFileContentWithoutChunkChecksums(FileChecksum fileChecksum) {
169                try (PreparedStatement preparedStatement = getStatement("filecontent.select.all.getFileContentByChecksumWithoutChunkChecksums.sql")) {
170                        preparedStatement.setString(1, fileChecksum.toString());
171
172                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
173                                if (resultSet.next()) {
174                                        FileContent fileContent = new FileContent();
175        
176                                        fileContent.setChecksum(FileChecksum.parseFileChecksum(resultSet.getString("checksum")));
177                                        fileContent.setSize(resultSet.getLong("size"));
178        
179                                        return fileContent;
180                                }
181                        }
182
183                        return null;
184                }
185                catch (SQLException e) {
186                        throw new RuntimeException(e);
187                }
188        }
189
190        private FileContent getFileContentWithChunkChecksums(FileChecksum fileChecksum) {
191                try (PreparedStatement preparedStatement = getStatement("filecontent.select.all.getFileContentByChecksumWithChunkChecksums.sql")) {
192                        preparedStatement.setString(1, fileChecksum.toString());
193
194                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
195                                FileContent fileContent = null;
196                                
197                                while (resultSet.next()) {
198                                        if (fileContent == null) {
199                                                fileContent = new FileContent();
200                                                
201                                                fileContent.setChecksum(FileChecksum.parseFileChecksum(resultSet.getString("checksum")));
202                                                fileContent.setSize(resultSet.getLong("size"));
203                                        }
204                                        
205                                        // Add chunk references
206                                        ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(resultSet.getString("chunk_checksum"));
207                                        fileContent.addChunk(chunkChecksum);
208                                }
209        
210                                return fileContent;
211                        }
212                }
213                catch (SQLException e) {
214                        throw new RuntimeException(e);
215                }
216        }
217        
218        private Map<FileChecksum, FileContent> createFileContents(ResultSet resultSet) throws SQLException {
219                Map<FileChecksum, FileContent> fileContents = new HashMap<FileChecksum, FileContent>(); 
220                FileChecksum currentFileChecksum = null;
221                
222                while (resultSet.next()) {              
223                        FileChecksum fileChecksum = FileChecksum.parseFileChecksum(resultSet.getString("checksum"));
224                        FileContent fileContent = null;
225                        
226                        if (currentFileChecksum != null && currentFileChecksum.equals(fileChecksum)) {
227                                fileContent = fileContents.get(fileChecksum);   
228                        }
229                        else {
230                                fileContent = new FileContent();
231                                
232                                fileContent.setChecksum(fileChecksum);
233                                fileContent.setSize(resultSet.getLong("size"));
234                        }
235                        
236                        ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(resultSet.getString("chunk_checksum"));
237                        fileContent.addChunk(chunkChecksum);
238
239                        fileContents.put(fileChecksum, fileContent); 
240                        currentFileChecksum = fileChecksum;
241                }
242                
243                return fileContents;
244        }
245
246        /**
247         * no commit
248         */
249        public void updateDirtyFileContentsNewDatabaseId(long newDatabaseVersionId) {
250                try (PreparedStatement preparedStatement = getStatement("filecontent.update.dirty.updateDirtyFileContentsNewDatabaseId.sql")) {
251                        preparedStatement.setLong(1, newDatabaseVersionId);
252                        preparedStatement.executeUpdate();
253                }
254                catch (SQLException e) {
255                        throw new RuntimeException(e);
256                }               
257        }
258}