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;
029import org.syncany.database.ChunkEntry.ChunkChecksum;
030import org.syncany.database.VectorClock;
031
032/**
033 * The chunk data access object (DAO) writes and queries the SQL database for information
034 * on {@link ChunkEntry}s. It translates the relational data in the "chunk" table to
035 * Java objects.
036 * 
037 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
038 */
039public class ChunkSqlDao extends AbstractSqlDao {
040        private Map<ChunkChecksum, ChunkEntry> chunkCache;
041
042        public ChunkSqlDao(Connection connection) {
043                super(connection);
044                this.chunkCache = null;
045        }
046
047        /**
048         * Writes a list of {@link ChunkEntry}s to the database using <code>INSERT</code>s and the given connection.
049         * 
050         * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query.
051         * 
052         * @param connection The connection used to execute the statements
053         * @param databaseVersionId 
054         * @param chunks List of {@link ChunkEntry}s to be inserted in the database
055         * @throws SQLException If the SQL statement fails
056         */
057        public void writeChunks(Connection connection, long databaseVersionId, Collection<ChunkEntry> chunks) throws SQLException {
058                if (chunks.size() > 0) {
059                        PreparedStatement preparedStatement = getStatement(connection, "chunk.insert.all.writeChunks.sql");
060
061                        for (ChunkEntry chunk : chunks) {
062                                preparedStatement.setString(1, chunk.getChecksum().toString());
063                                preparedStatement.setLong(2, databaseVersionId);
064                                preparedStatement.setInt(3, chunk.getSize());
065
066                                preparedStatement.addBatch();
067                        }
068
069                        preparedStatement.executeBatch();
070                        preparedStatement.close();
071                }
072        }       
073
074        /**
075         * Removes unreferenced chunks from the database. Unreferenced chunks are chunks
076         * that are not referenced by any file content or multichunk. 
077         * 
078         * <p>During the cleanup process, when file versions are deleted, unused chunks 
079         * are left over. This method removes these chunks from the database.
080         * 
081         * <p><b>Note:</b> This method executes, but <b>does not commit</b> the query. 
082         */
083        public void removeUnreferencedChunks() {
084                try (PreparedStatement preparedStatement = getStatement("chunk.delete.all.removeUnreferencesChunks.sql")) {
085                        preparedStatement.execute();
086                        preparedStatement.close();
087                }
088                catch (SQLException e) {
089                        throw new RuntimeException(e);
090                }
091        }
092        
093        /**
094         * Queries the database of a chunk with the given checksum. 
095         * 
096         * <p>Note: When first called, this method loads the <b>chunk cache</b> and keeps
097         * this cache until it is cleared explicitly with {@link #clearCache()}. 
098         * 
099         * <p>Also note that this method will return <code>null</code> if the chunk has been
100         * added after the cache has been filled. 
101         * 
102         * @param chunkChecksum Chunk checksum of the chunk to be selected
103         * @return Returns the chunk entry, or <code>null</code> if the chunk does not exist.
104         */     
105        public synchronized ChunkEntry getChunk(ChunkChecksum chunkChecksum) {
106                if (chunkCache == null) {
107                        loadChunkCache();
108                }
109
110                return chunkCache.get(chunkChecksum);
111        }
112        
113        /**
114         * Clears the chunk cache loaded by {@link #getChunk(ChunkChecksum) getChunk()}
115         * and resets the cache. If {@link #getChunk(ChunkChecksum) getChunk()} is called
116         * after the cache is cleared, it is re-populated.
117         */
118        public synchronized void clearCache() {
119                if (chunkCache != null) {
120                        chunkCache.clear();
121                        chunkCache = null;
122                }
123        }
124
125        /**
126         * Queries the SQL database for all chunks that <b>originally appeared</b> in the
127         * database version identified by the given vector clock.
128         * 
129         * <p><b>Note:</b> This method does <b>not</b> select all the chunks that are referenced
130         * in the database version. In particular, it <b>does not return</b> chunks that appeared
131         * in previous other database versions.
132         * 
133         * @param vectorClock Vector clock that identifies the database version
134         * @return Returns all chunks that originally belong to a database version
135         */
136        public Map<ChunkChecksum, ChunkEntry> getChunks(VectorClock vectorClock) {
137                try (PreparedStatement preparedStatement = getStatement("chunk.select.all.getChunksForDatabaseVersion.sql")) {
138                        preparedStatement.setString(1, vectorClock.toString());
139
140                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
141                                return createChunkEntries(resultSet);
142                        }
143                }
144                catch (SQLException e) {
145                        throw new RuntimeException(e);
146                }
147        }
148
149        protected Map<ChunkChecksum, ChunkEntry> createChunkEntries(ResultSet resultSet) throws SQLException {
150                Map<ChunkChecksum, ChunkEntry> chunks = new HashMap<ChunkChecksum, ChunkEntry>();
151
152                while (resultSet.next()) {
153                        ChunkEntry chunkEntry = createChunkEntryFromRow(resultSet);
154                        chunks.put(chunkEntry.getChecksum(), chunkEntry);
155                }
156
157                return chunks;
158        }
159
160        protected ChunkEntry createChunkEntryFromRow(ResultSet resultSet) throws SQLException {
161                ChunkChecksum chunkChecksum = ChunkChecksum.parseChunkChecksum(resultSet.getString("checksum"));
162                return new ChunkEntry(chunkChecksum, resultSet.getInt("size"));
163        }
164        
165        protected void loadChunkCache() {
166                try (PreparedStatement preparedStatement = getStatement("chunk.select.all.loadChunkCache.sql")) {
167                        try (ResultSet resultSet = preparedStatement.executeQuery()) {
168                                chunkCache = createChunkEntries(resultSet);
169                        }
170                }
171                catch (SQLException e) {
172                        throw new RuntimeException(e);
173                }
174        }
175        
176        /**
177         * no commit
178         */
179        public void updateDirtyChunksNewDatabaseId(long newDatabaseVersionId) {
180                try (PreparedStatement preparedStatement = getStatement("chunk.update.dirty.updateDirtyChunksNewDatabaseId.sql")) {
181                        preparedStatement.setLong(1, newDatabaseVersionId);
182                        preparedStatement.executeUpdate();
183                }
184                catch (SQLException e) {
185                        throw new RuntimeException(e);
186                }               
187        }
188}