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;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026
027import org.syncany.database.ChunkEntry.ChunkChecksum;
028import org.syncany.database.FileContent.FileChecksum;
029import org.syncany.database.FileVersion.FileStatus;
030import org.syncany.database.MultiChunkEntry.MultiChunkId;
031import org.syncany.database.PartialFileHistory.FileHistoryId;
032
033/**
034 * The database represents the internal file and chunk index of the application. It
035 * can be used to reference or load a full local database (local client) or a
036 * remote database (from a delta database file of another clients).
037 *
038 * <p>A database consists of a sorted list of {@link DatabaseVersion}s, i.e. it is a
039 * collection of changes to the local file system.
040 *
041 * <p>For convenience, the class also offers a set of functionality to select objects
042 * from the current accumulated database. Examples include {@link #getChunk(ChunkChecksum) getChunk()},
043 * {@link #getContent(FileChecksum) getContent()} and {@link #getMultiChunk(MultiChunkId) getMultiChunk()}.
044 *
045 * <p>To allow this convenience, a few caches are kept in memory, and updated whenever a
046 * database version is added or removed.
047 *
048 * @see DatabaseVersion
049 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
050 */
051public class MemoryDatabase {
052        private List<DatabaseVersion> databaseVersions;
053
054        // Caches
055        private DatabaseVersion fullDatabaseVersionCache;
056        private Map<String, PartialFileHistory> filenameHistoryCache;
057        private Map<VectorClock, DatabaseVersion> databaseVersionIdCache;
058        private Map<FileChecksum, List<PartialFileHistory>> contentChecksumFileHistoriesCache;
059
060        public MemoryDatabase() {
061                databaseVersions = new ArrayList<DatabaseVersion>();
062
063                // Caches
064                fullDatabaseVersionCache = new DatabaseVersion();
065                filenameHistoryCache = new HashMap<String, PartialFileHistory>();
066                databaseVersionIdCache = new HashMap<VectorClock, DatabaseVersion>();
067                contentChecksumFileHistoriesCache = new HashMap<FileChecksum, List<PartialFileHistory>>();
068        }
069
070        public DatabaseVersion getLastDatabaseVersion() {
071                if (databaseVersions.size() == 0) {
072                        return null;
073                }
074
075                return databaseVersions.get(databaseVersions.size() - 1);
076        }
077
078        public List<DatabaseVersion> getDatabaseVersions() {
079                return Collections.unmodifiableList(databaseVersions);
080        }
081
082        public DatabaseVersion getDatabaseVersion(VectorClock vectorClock) {
083                return databaseVersionIdCache.get(vectorClock);
084        }
085
086        public FileContent getContent(FileChecksum checksum) {
087                return (checksum != null) ? fullDatabaseVersionCache.getFileContent(checksum) : null;
088        }
089
090        public Object getChunk(ChunkChecksum checksum) {
091                return (checksum != null) ? fullDatabaseVersionCache.getChunk(checksum) : null;
092        }
093
094        public MultiChunkEntry getMultiChunk(MultiChunkId id) {
095                return fullDatabaseVersionCache.getMultiChunk(id);
096        }
097
098        /**
099         * Get a multichunk that this chunk is contained in.
100         */
101        public MultiChunkId getMultiChunkIdForChunk(ChunkChecksum chunk) {
102                return fullDatabaseVersionCache.getMultiChunkId(chunk);
103        }
104
105        public PartialFileHistory getFileHistory(String relativeFilePath) {
106                return filenameHistoryCache.get(relativeFilePath);
107        }
108
109        public List<PartialFileHistory> getFileHistories(FileChecksum fileContentChecksum) {
110                return contentChecksumFileHistoriesCache.get(fileContentChecksum);
111        }
112
113        public PartialFileHistory getFileHistory(FileHistoryId fileId) {
114                return fullDatabaseVersionCache.getFileHistory(fileId);
115        }
116
117        public Collection<PartialFileHistory> getFileHistories() {
118                return fullDatabaseVersionCache.getFileHistories();
119        }
120
121        public Collection<MultiChunkEntry> getMultiChunks() {
122                return fullDatabaseVersionCache.getMultiChunks();
123        }
124
125        public void addDatabaseVersion(DatabaseVersion databaseVersion) {
126                databaseVersions.add(databaseVersion);
127
128                // Populate caches
129                // WARNING: Do NOT reorder, order important!!
130                updateDatabaseVersionIdCache(databaseVersion);
131                updateFullDatabaseVersionCache(databaseVersion);
132                updateFilenameHistoryCache();
133                updateContentChecksumCache();
134        }
135
136        public void removeDatabaseVersion(DatabaseVersion databaseVersion) {
137                databaseVersions.remove(databaseVersion);
138
139                // Populate caches
140                // WARNING: Do NOT reorder, order important!!
141                updateFullDatabaseVersionCache();
142                updateDatabaseVersionIdCache();
143                updateFilenameHistoryCache();
144                updateContentChecksumCache();
145        }
146
147        // TODO [medium] Very inefficient. Always updates whole cache
148        private void updateContentChecksumCache() {
149                contentChecksumFileHistoriesCache.clear();
150
151                for (PartialFileHistory fullFileHistory : fullDatabaseVersionCache.getFileHistories()) {
152                        FileChecksum lastVersionChecksum = fullFileHistory.getLastVersion().getChecksum();
153
154                        if (lastVersionChecksum != null) {
155                                List<PartialFileHistory> historiesWithVersionsWithSameChecksum = contentChecksumFileHistoriesCache.get(lastVersionChecksum);
156
157                                // Create if it does not exist
158                                if (historiesWithVersionsWithSameChecksum == null) {
159                                        historiesWithVersionsWithSameChecksum = new ArrayList<PartialFileHistory>();
160                                }
161
162                                // Add to cache
163                                historiesWithVersionsWithSameChecksum.add(fullFileHistory);
164                                contentChecksumFileHistoriesCache.put(lastVersionChecksum, historiesWithVersionsWithSameChecksum);
165                        }
166                }
167
168        }
169
170        private void updateFilenameHistoryCache() {
171                // TODO [medium] Performance: This throws away the unchanged entries. It should only update new database version
172                filenameHistoryCache.clear();
173
174                for (PartialFileHistory cacheFileHistory : fullDatabaseVersionCache.getFileHistories()) {
175                        FileVersion lastVersion = cacheFileHistory.getLastVersion();
176                        String fileName = lastVersion.getPath();
177
178                        if (lastVersion.getStatus() != FileStatus.DELETED) {
179                                filenameHistoryCache.put(fileName, cacheFileHistory);
180                        }
181                }
182        }
183
184        private void updateDatabaseVersionIdCache(DatabaseVersion newDatabaseVersion) {
185                databaseVersionIdCache.put(newDatabaseVersion.getVectorClock(), newDatabaseVersion);
186        }
187
188        private void updateDatabaseVersionIdCache() {
189                databaseVersionIdCache.clear();
190
191                for (DatabaseVersion databaseVersion : databaseVersions) {
192                        updateDatabaseVersionIdCache(databaseVersion);
193                }
194        }
195
196        private void updateFullDatabaseVersionCache() {
197                fullDatabaseVersionCache = new DatabaseVersion();
198
199                for (DatabaseVersion databaseVersion : databaseVersions) {
200                        updateFullDatabaseVersionCache(databaseVersion);
201                }
202        }
203
204        private void updateFullDatabaseVersionCache(DatabaseVersion newDatabaseVersion) {
205                // Chunks
206                for (ChunkEntry sourceChunk : newDatabaseVersion.getChunks()) {
207                        if (fullDatabaseVersionCache.getChunk(sourceChunk.getChecksum()) == null) {
208                                fullDatabaseVersionCache.addChunk(sourceChunk);
209                        }
210                }
211
212                // Multichunks
213                for (MultiChunkEntry sourceMultiChunk : newDatabaseVersion.getMultiChunks()) {
214                        if (fullDatabaseVersionCache.getMultiChunk(sourceMultiChunk.getId()) == null) {
215                                fullDatabaseVersionCache.addMultiChunk(sourceMultiChunk);
216                        }
217                }
218
219                // Contents
220                for (FileContent sourceFileContent : newDatabaseVersion.getFileContents()) {
221                        if (fullDatabaseVersionCache.getFileContent(sourceFileContent.getChecksum()) == null) {
222                                fullDatabaseVersionCache.addFileContent(sourceFileContent);
223                        }
224                }
225
226                // Histories
227                for (PartialFileHistory sourceFileHistory : newDatabaseVersion.getFileHistories()) {
228                        PartialFileHistory targetFileHistory = fullDatabaseVersionCache.getFileHistory(sourceFileHistory.getFileHistoryId());
229
230                        if (targetFileHistory == null) {
231                                fullDatabaseVersionCache.addFileHistory(sourceFileHistory.clone());
232                        }
233                        else {
234                                for (FileVersion sourceFileVersion : sourceFileHistory.getFileVersions().values()) {
235                                        if (targetFileHistory.getFileVersion(sourceFileVersion.getVersion()) == null) {
236                                                targetFileHistory.addFileVersion(sourceFileVersion);
237                                        }
238                                }
239                        }
240                }
241        }
242
243}