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.operations.down;
019
020import java.io.File;
021import java.io.IOException;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025
026import org.syncany.database.DatabaseVersion;
027import org.syncany.database.DatabaseVersionHeader;
028import org.syncany.database.MemoryDatabase;
029import org.syncany.database.VectorClock;
030import org.syncany.database.dao.DatabaseXmlSerializer;
031import org.syncany.database.dao.DatabaseXmlSerializer.DatabaseReadType;
032
033/**
034 * The DatabaseFileReader provides a way to read a series of database files
035 * in a memory-efficient way, by converting them to a series of MemoryDatabases,
036 * none of which are too large.
037 * 
038 * @author Pim Otte
039 */
040public class DatabaseFileReader implements Iterator<MemoryDatabase> {
041        private static final int MAX_FILES = 9999;
042
043        private DatabaseXmlSerializer databaseSerializer;
044        private List<DatabaseVersionHeader> winnersApplyBranchList;
045        private Map<DatabaseVersionHeader, File> databaseVersionLocations;
046        private int branchIndex = 0;
047
048        public DatabaseFileReader(DatabaseXmlSerializer databaseSerializer, DatabaseBranch winnersApplyBranch,
049                        Map<DatabaseVersionHeader, File> databaseVersionLocations) {
050                
051                this.winnersApplyBranchList = winnersApplyBranch.getAll();
052                this.databaseVersionLocations = databaseVersionLocations;
053                this.databaseSerializer = databaseSerializer;
054        }
055
056        public boolean hasNext() {
057                return branchIndex < winnersApplyBranchList.size();
058        }
059
060        /**
061         * Loads the winner's database branch into the memory in a {@link MemoryDatabase} object, by using
062         * the already downloaded list of remote database files.
063         *
064         * <p>Because database files can contain multiple {@link DatabaseVersion}s per client, a range for which
065         * to load the database versions must be determined.
066         *
067         * <p><b>Example 1:</b><br>
068         * <pre>
069         *  db-A-0001   (A1)     Already known             Not loaded
070         *  db-A-0005   (A2)     Already known             Not loaded
071         *              (A3)     Already known             Not loaded
072         *              (A4)     Part of winner's branch   Loaded
073         *              (A5)     Purge database version    Ignored (only DEFAULT)
074         *  db-B-0001   (A5,B1)  Part of winner's branch   Loaded
075         *  db-A-0006   (A6,B1)  Part of winner's branch   Loaded
076         * </pre>
077         *
078         * <p>In example 1, only (A4)-(A5) must be loaded from db-A-0005, and not all four database versions.
079         *
080         * <p><b>Other example:</b><br>
081         * <pre>
082         *  db-A-0005   (A1)     Part of winner's branch   Loaded
083         *  db-A-0005   (A2)     Part of winner's branch   Loaded
084         *  db-B-0001   (A2,B1)  Part of winner's branch   Loaded
085         *  db-A-0005   (A3,B1)  Part of winner's branch   Loaded
086         *  db-A-0005   (A4,B1)  Part of winner's branch   Loaded
087         *  db-A-0005   (A5,B1)  Purge database version    Ignored (only DEFAULT)
088         * </pre>
089         *
090         * <p>In example 2, (A1)-(A5,B1) [except (A2,B1)] are contained in db-A-0005 (after merging!), so
091         * db-A-0005 must be processed twice; each time loading separate parts of the file. In this case:
092         * First load (A1)-(A2) from db-A-0005, then load (A2,B1) from db-B-0001, then load (A3,B1)-(A4,B1)
093         * from db-A-0005, and ignore (A5,B1).
094         *
095         * @return Returns a loaded memory database containing all metadata from the winner's branch
096         */
097        @Override
098        public MemoryDatabase next() {
099                MemoryDatabase winnerBranchDatabase = new MemoryDatabase();
100                String rangeClientName = null;
101                VectorClock rangeVersionFrom = null;
102                VectorClock rangeVersionTo = null;
103
104                while (branchIndex < winnersApplyBranchList.size() && winnerBranchDatabase.getFileHistories().size() < MAX_FILES) {
105                        DatabaseVersionHeader currentDatabaseVersionHeader = winnersApplyBranchList.get(branchIndex);
106                        DatabaseVersionHeader nextDatabaseVersionHeader = (branchIndex + 1 < winnersApplyBranchList.size()) ? winnersApplyBranchList
107                                        .get(branchIndex + 1) : null;
108
109                        // First of range for this client
110                        if (rangeClientName == null) {
111                                rangeClientName = currentDatabaseVersionHeader.getClient();
112                                rangeVersionFrom = currentDatabaseVersionHeader.getVectorClock();
113                                rangeVersionTo = currentDatabaseVersionHeader.getVectorClock();
114                        }
115
116                        // Still in range for this client
117                        else {
118                                rangeVersionTo = currentDatabaseVersionHeader.getVectorClock();
119                        }
120
121                        // Now load this stuff from the database file (or not)
122                        //   - If the database file exists, load the range and reset it
123                        //   - If not, only force a load if this is the range end
124
125                        File databaseVersionFile = databaseVersionLocations.get(currentDatabaseVersionHeader);
126
127                        if (databaseVersionFile == null) {
128                                throw new RuntimeException("Could not find file corresponding to " + currentDatabaseVersionHeader
129                                                + ", while it is in the winners branch.");
130                        }
131
132                        boolean lastDatabaseVersionHeader = nextDatabaseVersionHeader == null;
133                        boolean nextDatabaseVersionInSameFile = lastDatabaseVersionHeader
134                                        || databaseVersionFile.equals(databaseVersionLocations.get(nextDatabaseVersionHeader));
135                        boolean rangeEnds = lastDatabaseVersionHeader || !nextDatabaseVersionInSameFile;
136
137                        if (rangeEnds) {
138                                try {
139                                        databaseSerializer.load(winnerBranchDatabase, databaseVersionFile, rangeVersionFrom, rangeVersionTo, DatabaseReadType.FULL);
140                                }
141                                catch (IOException e) {
142                                        throw new RuntimeException(e.getMessage(), e);
143                                }
144                                rangeClientName = null;
145                        }
146                        branchIndex++;
147                }
148
149                return winnerBranchDatabase;
150        }
151
152        @Override
153        public void remove() {
154                throw new UnsupportedOperationException("Removing a databaseversion is not supported");
155
156        }
157
158}