DatabaseFileReader.java

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.operations.down;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.syncany.database.DatabaseVersion;
import org.syncany.database.DatabaseVersionHeader;
import org.syncany.database.MemoryDatabase;
import org.syncany.database.VectorClock;
import org.syncany.database.dao.DatabaseXmlSerializer;
import org.syncany.database.dao.DatabaseXmlSerializer.DatabaseReadType;

/**
 * The DatabaseFileReader provides a way to read a series of database files
 * in a memory-efficient way, by converting them to a series of MemoryDatabases,
 * none of which are too large.
 * 
 * @author Pim Otte
 */
public class DatabaseFileReader implements Iterator<MemoryDatabase> {
	private static final int MAX_FILES = 9999;

	private DatabaseXmlSerializer databaseSerializer;
	private List<DatabaseVersionHeader> winnersApplyBranchList;
	private Map<DatabaseVersionHeader, File> databaseVersionLocations;
	private int branchIndex = 0;

	public DatabaseFileReader(DatabaseXmlSerializer databaseSerializer, DatabaseBranch winnersApplyBranch,
			Map<DatabaseVersionHeader, File> databaseVersionLocations) {
		
		this.winnersApplyBranchList = winnersApplyBranch.getAll();
		this.databaseVersionLocations = databaseVersionLocations;
		this.databaseSerializer = databaseSerializer;
	}

	public boolean hasNext() {
		return branchIndex < winnersApplyBranchList.size();
	}

	/**
	 * Loads the winner's database branch into the memory in a {@link MemoryDatabase} object, by using
	 * the already downloaded list of remote database files.
	 *
	 * <p>Because database files can contain multiple {@link DatabaseVersion}s per client, a range for which
	 * to load the database versions must be determined.
	 *
	 * <p><b>Example 1:</b><br>
	 * <pre>
	 *  db-A-0001   (A1)     Already known             Not loaded
	 *  db-A-0005   (A2)     Already known             Not loaded
	 *              (A3)     Already known             Not loaded
	 *              (A4)     Part of winner's branch   Loaded
	 *              (A5)     Purge database version    Ignored (only DEFAULT)
	 *  db-B-0001   (A5,B1)  Part of winner's branch   Loaded
	 *  db-A-0006   (A6,B1)  Part of winner's branch   Loaded
	 * </pre>
	 *
	 * <p>In example 1, only (A4)-(A5) must be loaded from db-A-0005, and not all four database versions.
	 *
	 * <p><b>Other example:</b><br>
	 * <pre>
	 *  db-A-0005   (A1)     Part of winner's branch   Loaded
	 *  db-A-0005   (A2)     Part of winner's branch   Loaded
	 *  db-B-0001   (A2,B1)  Part of winner's branch   Loaded
	 *  db-A-0005   (A3,B1)  Part of winner's branch   Loaded
	 *  db-A-0005   (A4,B1)  Part of winner's branch   Loaded
	 *  db-A-0005   (A5,B1)  Purge database version    Ignored (only DEFAULT)
	 * </pre>
	 *
	 * <p>In example 2, (A1)-(A5,B1) [except (A2,B1)] are contained in db-A-0005 (after merging!), so
	 * db-A-0005 must be processed twice; each time loading separate parts of the file. In this case:
	 * First load (A1)-(A2) from db-A-0005, then load (A2,B1) from db-B-0001, then load (A3,B1)-(A4,B1)
	 * from db-A-0005, and ignore (A5,B1).
	 *
	 * @return Returns a loaded memory database containing all metadata from the winner's branch
	 */
	@Override
	public MemoryDatabase next() {
		MemoryDatabase winnerBranchDatabase = new MemoryDatabase();
		String rangeClientName = null;
		VectorClock rangeVersionFrom = null;
		VectorClock rangeVersionTo = null;

		while (branchIndex < winnersApplyBranchList.size() && winnerBranchDatabase.getFileHistories().size() < MAX_FILES) {
			DatabaseVersionHeader currentDatabaseVersionHeader = winnersApplyBranchList.get(branchIndex);
			DatabaseVersionHeader nextDatabaseVersionHeader = (branchIndex + 1 < winnersApplyBranchList.size()) ? winnersApplyBranchList
					.get(branchIndex + 1) : null;

			// First of range for this client
			if (rangeClientName == null) {
				rangeClientName = currentDatabaseVersionHeader.getClient();
				rangeVersionFrom = currentDatabaseVersionHeader.getVectorClock();
				rangeVersionTo = currentDatabaseVersionHeader.getVectorClock();
			}

			// Still in range for this client
			else {
				rangeVersionTo = currentDatabaseVersionHeader.getVectorClock();
			}

			// Now load this stuff from the database file (or not)
			//   - If the database file exists, load the range and reset it
			//   - If not, only force a load if this is the range end

			File databaseVersionFile = databaseVersionLocations.get(currentDatabaseVersionHeader);

			if (databaseVersionFile == null) {
				throw new RuntimeException("Could not find file corresponding to " + currentDatabaseVersionHeader
						+ ", while it is in the winners branch.");
			}

			boolean lastDatabaseVersionHeader = nextDatabaseVersionHeader == null;
			boolean nextDatabaseVersionInSameFile = lastDatabaseVersionHeader
					|| databaseVersionFile.equals(databaseVersionLocations.get(nextDatabaseVersionHeader));
			boolean rangeEnds = lastDatabaseVersionHeader || !nextDatabaseVersionInSameFile;

			if (rangeEnds) {
				try {
					databaseSerializer.load(winnerBranchDatabase, databaseVersionFile, rangeVersionFrom, rangeVersionTo, DatabaseReadType.FULL);
				}
				catch (IOException e) {
					throw new RuntimeException(e.getMessage(), e);
				}
				rangeClientName = null;
			}
			branchIndex++;
		}

		return winnerBranchDatabase;
	}

	@Override
	public void remove() {
		throw new UnsupportedOperationException("Removing a databaseversion is not supported");

	}

}