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.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.syncany.chunk.MultiChunk;
033import org.syncany.database.ChunkEntry.ChunkChecksum;
034import org.syncany.database.DatabaseVersion.DatabaseVersionStatus;
035import org.syncany.database.DatabaseVersionHeader;
036import org.syncany.database.FileContent.FileChecksum;
037import org.syncany.database.MultiChunkEntry;
038import org.syncany.database.MultiChunkEntry.MultiChunkId;
039import org.syncany.database.VectorClock;
040
041/**
042 * The multi-chunk data access object (DAO) queries and modifies the <i>multichunk</i> and
043 * <i>multichunk_chunk</i> table in the SQL database. These tables correspond to the Java
044 * object {@link MultiChunk}.
045 * 
046 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
047 */
048public class MultiChunkSqlDao extends AbstractSqlDao {
049        public MultiChunkSqlDao(Connection connection) {
050                super(connection);
051        }
052
053        public void writeMultiChunks(Connection connection, long databaseVersionId, Collection<MultiChunkEntry> multiChunks) throws SQLException {
054                for (MultiChunkEntry multiChunk : multiChunks) {
055                        PreparedStatement preparedStatement = getStatement(connection, "multichunk.insert.all.writeMultiChunks.sql");
056
057                        preparedStatement.setString(1, multiChunk.getId().toString());
058                        preparedStatement.setLong(2, databaseVersionId);
059                        preparedStatement.setLong(3, multiChunk.getSize());
060                        
061                        preparedStatement.executeUpdate();
062                        preparedStatement.close();
063                        
064                        writeMultiChunkRefs(connection, multiChunk);                    
065                }
066        }
067
068        private void writeMultiChunkRefs(Connection connection, MultiChunkEntry multiChunk) throws SQLException {
069                PreparedStatement preparedStatement = getStatement("multichunk.insert.all.writeMultiChunkRefs.sql");
070                
071                for (ChunkChecksum chunkChecksum : multiChunk.getChunks()) {
072                        preparedStatement.setString(1, multiChunk.getId().toString());
073                        preparedStatement.setString(2, chunkChecksum.toString());
074                        
075                        preparedStatement.addBatch();                   
076                }
077                
078                preparedStatement.executeBatch();
079                preparedStatement.close();
080        }       
081
082        public void writeMuddyMultiChunks(Map<DatabaseVersionHeader, Collection<MultiChunkEntry>> muddyMultiChunksPerDatabaseVersion) throws SQLException {
083                PreparedStatement preparedStatement = getStatement("multichunk_muddy.insert.muddy.writeMuddyMultiChunks.sql");
084                
085                for (DatabaseVersionHeader muddyDatabaseVersionHeader : muddyMultiChunksPerDatabaseVersion.keySet()) {
086                        Collection<MultiChunkEntry> muddyMultiChunks = muddyMultiChunksPerDatabaseVersion.get(muddyDatabaseVersionHeader);
087                        
088                        for (MultiChunkEntry muddyMultiChunk : muddyMultiChunks) {
089                                String multiChunkIdStr = muddyMultiChunk.getId().toString();
090                                String clientName = muddyDatabaseVersionHeader.getClient();
091                                Long clientVersion = muddyDatabaseVersionHeader.getVectorClock().getClock(clientName);
092                                
093                                preparedStatement.setString(1, multiChunkIdStr);
094                                preparedStatement.setString(2, clientName);
095                                preparedStatement.setLong(3, clientVersion);
096                                
097                                preparedStatement.addBatch();   
098                        }
099                }
100                
101                preparedStatement.executeBatch();
102                preparedStatement.close();
103        }
104
105        public void removeUnreferencedMultiChunks() throws SQLException {
106                // Note: Chunk references (multichunk_chunk) must be removed first, because
107                //       of the foreign key constraints. 
108                
109                removeUnreferencedMultiChunkChunkRefs();
110                removeUnreferencedMultiChunksInt();
111        }
112        
113        private void removeUnreferencedMultiChunksInt() throws SQLException {
114                PreparedStatement preparedStatement = getStatement("multichunk.delete.all.removeUnreferencedMultiChunks.sql");
115                preparedStatement.executeUpdate();      
116                preparedStatement.close();
117        }
118        
119        private void removeUnreferencedMultiChunkChunkRefs() throws SQLException {
120                PreparedStatement preparedStatement = getStatement("multichunk.delete.all.removeUnreferencedMultiChunkChunkRefs.sql");
121                preparedStatement.executeUpdate();      
122                preparedStatement.close();
123        }
124        
125        public void removeNonMuddyMultiChunks() throws SQLException {
126                PreparedStatement preparedStatement = getStatement("multichunk_muddy.delete.muddy.removeNonMuddyMultiChunks.sql");
127                preparedStatement.executeUpdate();      
128                preparedStatement.close();
129        }
130        
131        /**
132         * Note: This method selects also {@link DatabaseVersionStatus#DIRTY DIRTY}.
133         */
134        public List<MultiChunkId> getMultiChunkIds(FileChecksum fileChecksum) {
135                List<MultiChunkId> multiChunkIds = new ArrayList<MultiChunkId>();
136
137                if (fileChecksum == null) {
138                        return multiChunkIds;
139                }
140                else {
141                        try (PreparedStatement preparedStatement = getStatement("multichunk.select.all.getMultiChunkIdsForFileChecksum.sql")) {
142                                preparedStatement.setString(1, fileChecksum.toString());
143        
144                                try (ResultSet resultSet = preparedStatement.executeQuery()) {
145                                        while (resultSet.next()) {
146                                                multiChunkIds.add(MultiChunkId.parseMultiChunkId(resultSet.getString("multichunk_id")));
147                                        }
148                
149                                        return multiChunkIds;
150                                }
151                        }
152                        catch (SQLException e) {
153                                throw new RuntimeException(e);
154                        }
155                }
156        }       
157        
158        /**
159         * Note: This method selects also {@link DatabaseVersionStatus#DIRTY DIRTY}.
160         */
161        public Map<MultiChunkId, MultiChunkEntry> getMultiChunks(VectorClock vectorClock) {
162                try (PreparedStatement preparedStatement = getStatement("multichunk.select.all.getMultiChunksWithChunksForDatabaseVersion.sql")) {
163                        preparedStatement.setString(1, vectorClock.toString());
164
165                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
166                                return createMultiChunkEntriesWithChunks(resultSet);
167                        }
168                }
169                catch (SQLException e) {
170                        throw new RuntimeException(e);
171                }
172        }       
173
174        /**
175         * no commit
176         */
177        public void updateDirtyMultiChunksNewDatabaseId(long newDatabaseVersionId) {
178                try (PreparedStatement preparedStatement = getStatement("multichunk.update.dirty.updateDirtyMultiChunksNewDatabaseId.sql")) {
179                        preparedStatement.setLong(1, newDatabaseVersionId);
180                        preparedStatement.executeUpdate();
181                }
182                catch (SQLException e) {
183                        throw new RuntimeException(e);
184                }               
185        }
186        
187        /**
188         * Note: This method selects also {@link DatabaseVersionStatus#DIRTY DIRTY}.
189         */
190        public MultiChunkId getMultiChunkId(ChunkChecksum chunkChecksum) {
191                try (PreparedStatement preparedStatement = getStatement("multichunk.select.all.getMultiChunkIdForChunk.sql")) {
192                        preparedStatement.setString(1, chunkChecksum.toString());
193                                        
194                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
195                                if (resultSet.next()) {
196                                        return MultiChunkId.parseMultiChunkId(resultSet.getString("multichunk_id"));
197                                }
198                        }
199
200                        return null;
201                }
202                catch (SQLException e) {
203                        throw new RuntimeException(e);
204                }
205        }
206        
207        /**
208         * Note: This method selects also {@link DatabaseVersionStatus#DIRTY DIRTY}.
209         */
210        public Map<ChunkChecksum,MultiChunkId> getMultiChunkIdsByChecksums(List<ChunkChecksum> chunkChecksums) {
211                // Gather a unique array of checksum strings (required for query!)
212                Set<ChunkChecksum> chunkChecksumSet = new HashSet<ChunkChecksum>(chunkChecksums);
213                String[] checksums = new String[chunkChecksumSet.size()];
214                int i = 0;
215                for (ChunkChecksum checksum : chunkChecksumSet) {
216                        checksums[i] = checksum.toString();
217                        i++;
218                }
219                
220                // Execute query
221                Map<ChunkChecksum, MultiChunkId> result = new HashMap<ChunkChecksum, MultiChunkId>();
222                try (PreparedStatement preparedStatement = getStatement("multichunk.select.all.getMultiChunkIdForChunks.sql")) {
223                        preparedStatement.setArray(1, connection.createArrayOf("varchar", checksums));  
224                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
225                                while (resultSet.next()) {
226                                        result.put(ChunkChecksum.parseChunkChecksum(resultSet.getString("chunk_checksum")),
227                                                        MultiChunkId.parseMultiChunkId(resultSet.getString("multichunk_id")));
228                                        
229                                }
230                        }
231
232                        return result;
233                }
234                catch (SQLException e) {
235                        throw new RuntimeException(e);
236                }
237        }
238        
239        public List<MultiChunkId> getDirtyMultiChunkIds() {
240                List<MultiChunkId> dirtyMultiChunkIds = new ArrayList<MultiChunkId>();          
241                
242                try (PreparedStatement preparedStatement = getStatement("multichunk.select.dirty.getDirtyMultiChunkIds.sql")) {
243                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
244                                while (resultSet.next()) {
245                                        dirtyMultiChunkIds.add(MultiChunkId.parseMultiChunkId(resultSet.getString("multichunk_id")));
246                                }
247                                
248                                return dirtyMultiChunkIds;
249                        }
250                }
251                catch (SQLException e) {
252                        throw new RuntimeException(e);
253                }
254        }
255        
256        public Map<MultiChunkId, MultiChunkEntry> getUnusedMultiChunks() {
257                try (PreparedStatement preparedStatement = getStatement("multichunk.select.all.getUnusedMultiChunks.sql")) {
258                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
259                                return createMultiChunkEntriesWithoutChunks(resultSet);
260                        }
261                }
262                catch (SQLException e) {
263                        throw new RuntimeException(e);
264                }
265        }
266
267        private Map<MultiChunkId, MultiChunkEntry> createMultiChunkEntriesWithoutChunks(ResultSet resultSet) throws SQLException {              
268                Map<MultiChunkId, MultiChunkEntry> unusedMultiChunkIds = new HashMap<MultiChunkId, MultiChunkEntry>();          
269                
270                while (resultSet.next()) {
271                        MultiChunkId multiChunkId = MultiChunkId.parseMultiChunkId(resultSet.getString("id"));
272                        long multiChunkSize = resultSet.getLong("size");
273                                                
274                        unusedMultiChunkIds.put(multiChunkId, new MultiChunkEntry(multiChunkId, multiChunkSize));
275                }
276                
277                return unusedMultiChunkIds;
278        }
279        
280        private Map<MultiChunkId, MultiChunkEntry> createMultiChunkEntriesWithChunks(ResultSet resultSet) throws SQLException {
281                Map<MultiChunkId, MultiChunkEntry> multiChunkEntries = new HashMap<MultiChunkId, MultiChunkEntry>();            
282                MultiChunkId currentMultiChunkId = null;
283                
284                while (resultSet.next()) {                      
285                        MultiChunkId multiChunkId = MultiChunkId.parseMultiChunkId(resultSet.getString("multichunk_id"));
286                        long multiChunkSize = resultSet.getLong("size");
287                        
288                        MultiChunkEntry multiChunkEntry = null;
289                        
290                        if (currentMultiChunkId != null && currentMultiChunkId.equals(multiChunkId)) {
291                                multiChunkEntry = multiChunkEntries.get(multiChunkId);  
292                        }
293                        else {
294                                multiChunkEntry = new MultiChunkEntry(multiChunkId, multiChunkSize);
295                        }
296                        
297                        multiChunkEntry.addChunk(ChunkChecksum.parseChunkChecksum(resultSet.getString("chunk_checksum")));
298                        multiChunkEntries.put(multiChunkId, multiChunkEntry); 
299                        
300                        currentMultiChunkId = multiChunkId;
301                }
302                
303                return multiChunkEntries;
304        }
305}