WatchServer.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.daemon;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.syncany.config.Config;
import org.syncany.config.ConfigException;
import org.syncany.config.ConfigHelper;
import org.syncany.config.DaemonConfigHelper;
import org.syncany.config.LocalEventBus;
import org.syncany.config.to.DaemonConfigTO;
import org.syncany.config.to.FolderTO;
import org.syncany.operations.daemon.Watch.SyncStatus;
import org.syncany.operations.daemon.messages.AddWatchManagementRequest;
import org.syncany.operations.daemon.messages.AddWatchManagementResponse;
import org.syncany.operations.daemon.messages.BadRequestResponse;
import org.syncany.operations.daemon.messages.DaemonReloadedExternalEvent;
import org.syncany.operations.daemon.messages.ListWatchesManagementRequest;
import org.syncany.operations.daemon.messages.ListWatchesManagementResponse;
import org.syncany.operations.daemon.messages.RemoveWatchManagementRequest;
import org.syncany.operations.daemon.messages.RemoveWatchManagementResponse;
import org.syncany.operations.daemon.messages.api.FolderRequest;
import org.syncany.operations.daemon.messages.api.ManagementRequest;
import org.syncany.operations.daemon.messages.api.ManagementRequestHandler;
import org.syncany.operations.daemon.messages.api.Response;
import org.syncany.operations.watch.WatchOperation;
import org.syncany.operations.watch.WatchOperationOptions;

import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;

/**
 * The watch server can manage many different {@link WatchOperation}s. When started
 * with {@link #start(DaemonConfigTO)} or {@link #reload(DaemonConfigTO)}, it first reads the daemon configuration file
 * and then runs new threads for each configured Syncany folder. Invalid or non-existing folders
 * are ignored.
 *
 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
 */
public class WatchServer {
	private static final Logger logger = Logger.getLogger(WatchServer.class.getSimpleName());

	private DaemonConfigTO daemonConfig;
	private Map<File, WatchRunner> watchOperations;
	private LocalEventBus eventBus;

	public WatchServer() {
		this.daemonConfig = null;
		this.watchOperations = new TreeMap<File, WatchRunner>();

		this.eventBus = LocalEventBus.getInstance();
		this.eventBus.register(this);
	}

	public void start(DaemonConfigTO daemonConfigTO) {
		reload(daemonConfigTO);
	}

	public void reload(DaemonConfigTO daemonConfigTO) {
		logger.log(Level.INFO, "Starting/reloading watch server ... ");

		// Update config
		daemonConfig = daemonConfigTO;

		// Restart threads
		try {
			Map<File, FolderTO> watchedFolders = getFolderMap(daemonConfigTO.getFolders());

			stopAllWatchOperations();
			startWatchOperations(watchedFolders);

			fireDaemonReloadedEvent();
		}
		catch (Exception e) {
			logger.log(Level.WARNING, "Cannot (re-)load config. Exception thrown.", e);
		}
	}

	public void stop() {
		logger.log(Level.INFO, "Stopping watch server ...  ");
		Map<File, WatchRunner> copyOfWatchOperations = Maps.newHashMap(watchOperations);

		for (Map.Entry<File, WatchRunner> folderEntry : copyOfWatchOperations.entrySet()) {
			File localDir = folderEntry.getKey();
			WatchRunner watchOperationThread = folderEntry.getValue();

			logger.log(Level.INFO, "- Stopping watch operation at " + localDir + " ...");
			watchOperationThread.stop();

			watchOperations.remove(localDir);
		}
	}

	private void startWatchOperations(Map<File, FolderTO> newWatchedFolderTOs) throws ConfigException, ServiceAlreadyStartedException {
		for (Map.Entry<File, FolderTO> folderEntry : newWatchedFolderTOs.entrySet()) {
			File localDir = folderEntry.getKey();

			try {
				Config watchConfig = ConfigHelper.loadConfig(localDir);

				if (watchConfig != null) {
					logger.log(Level.INFO, "- Starting watch operation at " + localDir + " ...");

					WatchOperationOptions watchOptions = folderEntry.getValue().getWatchOptions();

					if (watchOptions == null) {
						watchOptions = new WatchOperationOptions();
					}

					WatchRunner watchRunner = new WatchRunner(watchConfig, watchOptions, daemonConfig.getPortTO());
					watchRunner.start();

					watchOperations.put(localDir, watchRunner);
				}
				else {
					logger.log(Level.INFO, "- CANNOT start watch, because no config found at " + localDir + " ...");
				}
			}
			catch (Exception e) {
				logger.log(Level.SEVERE, "  + Cannot start watch operation at " + localDir + ". IGNORING.", e);
			}
		}
	}

	/**
	 * Stops all watchOperations and verifies if
	 * they actually have stopped.
	 */
	private void stopAllWatchOperations() {
		for (File localDir : watchOperations.keySet()) {
			WatchRunner watchOperationThread = watchOperations.get(localDir);

			logger.log(Level.INFO, "- Stopping watch operation at " + localDir + " ...");
			watchOperationThread.stop();
		}

		// Check if watch operations actually have stopped.
		while (watchOperations.keySet().size() > 0) {
			Map<File, WatchRunner> watchOperationsCopy = new TreeMap<File, WatchRunner>(watchOperations);

			for (File localDir : watchOperationsCopy.keySet()) {
				WatchRunner watchOperationThread = watchOperationsCopy.get(localDir);

				if (watchOperationThread.hasStopped()) {
					logger.log(Level.INFO, "- Watch operation at " + localDir + " has stopped");
					watchOperations.remove(localDir);
				}
			}
		}
	}

	private Map<File, FolderTO> getFolderMap(List<FolderTO> watchedFolders) {
		Map<File, FolderTO> watchedFolderTOs = new TreeMap<File, FolderTO>();

		for (FolderTO folderTO : watchedFolders) {
			if (folderTO.isEnabled()) {
				watchedFolderTOs.put(new File(folderTO.getPath()), folderTO);
			}
		}

		return watchedFolderTOs;
	}

	private void fireDaemonReloadedEvent() {
		logger.log(Level.INFO, "Firing daemon-reloaded event ...");
		eventBus.post(new DaemonReloadedExternalEvent());
	}

	@Subscribe
	public void onFolderRequestReceived(FolderRequest folderRequest) {
		File rootFolder = new File(folderRequest.getRoot());

		if (!watchOperations.containsKey(rootFolder)) {
			eventBus.post(new BadRequestResponse(folderRequest.getId(), "Unknown root folder."));
		}
	}
	
	@Subscribe
	public void onManagementRequestReceived(ManagementRequest managementRequest) {
		logger.log(Level.INFO, "Received " + managementRequest);

		try {
			ManagementRequestHandler handler = ManagementRequestHandler.createManagementRequestHandler(managementRequest);
			Response response = handler.handleRequest(managementRequest);

			if (response != null) {
				eventBus.post(response);
			}
		}
		catch (ClassNotFoundException e) {
			logger.log(Level.FINE, "No handler found for management request class " + managementRequest.getClass() + ". Ignoring."); // Not logging 'e'!
		}
		catch (Exception e) {
			logger.log(Level.FINE, "Failed to process request", e);
			eventBus.post(new BadRequestResponse(managementRequest.getId(), "Invalid request."));
		}
	}

	@Subscribe
	public void onListWatchesRequestReceived(ListWatchesManagementRequest request) {
		List<Watch> watchList = new ArrayList<Watch>();

		for (File watchFolder : watchOperations.keySet()) {
			boolean syncRunning = watchOperations.get(watchFolder).isSyncRunning();
			SyncStatus syncStatus = (syncRunning) ? SyncStatus.SYNCING : SyncStatus.IN_SYNC;

			watchList.add(new Watch(watchFolder, syncStatus));
		}

		eventBus.post(new ListWatchesManagementResponse(request.getId(), watchList));
	}

	@Subscribe
	public void onAddWatchRequestReceived(AddWatchManagementRequest request) {
		File rootFolder = request.getWatch();

		if (watchOperations.containsKey(rootFolder)) {
			eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_ALREADY_EXISTS, request.getId(), "Watch already exists."));
		}
		else {
			try {
				boolean folderAdded = DaemonConfigHelper.addFolder(rootFolder);

				if (folderAdded) {
					eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.OKAY, request.getId(), "Successfully added."));
				}
				else {
					eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_ALREADY_EXISTS, request.getId(),
							"Watch already exists (inactive/disabled)."));
				}
			}
			catch (ConfigException e) {
				logger.log(Level.WARNING, "Error adding watch to daemon config.", e);
				eventBus.post(new AddWatchManagementResponse(AddWatchManagementResponse.ERR_OTHER, request.getId(), "Error adding to config: "
						+ e.getMessage()));
			}
		}
	}
	
	@Subscribe
	public void onRemoveWatchRequestReceived(RemoveWatchManagementRequest request) {
		File rootFolder = request.getWatch();

		if (!watchOperations.containsKey(rootFolder)) {
			eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_DOES_NOT_EXIST, request.getId(), "Watch does not exist."));
		}
		else {
			try {
				boolean folderRemoved = DaemonConfigHelper.removeFolder(rootFolder);

				if (folderRemoved) {
					eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.OKAY, request.getId(), "Successfully removed."));
				}
				else {
					eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_DOES_NOT_EXIST, request.getId(),
							"Watch does not exist (inactive/disabled)."));
				}
			}
			catch (ConfigException e) {
				logger.log(Level.WARNING, "Error removing watch from daemon config.", e);
				eventBus.post(new RemoveWatchManagementResponse(RemoveWatchManagementResponse.ERR_OTHER, request.getId(), "Error removing to config: "
						+ e.getMessage()));
			}
		}
	}
}