PluginCommand.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.cli;

import static java.util.Arrays.asList;

import java.util.ArrayList;
import java.util.List;

import org.syncany.cli.util.CliTableUtil;
import org.syncany.operations.OperationResult;
import org.syncany.operations.daemon.messages.ConnectToHostExternalEvent;
import org.syncany.operations.daemon.messages.PluginInstallExternalEvent;
import org.syncany.operations.plugin.ExtendedPluginInfo;
import org.syncany.operations.plugin.PluginInfo;
import org.syncany.operations.plugin.PluginOperation;
import org.syncany.operations.plugin.PluginOperationAction;
import org.syncany.operations.plugin.PluginOperationOptions;
import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode;
import org.syncany.operations.plugin.PluginOperationResult;
import org.syncany.operations.plugin.PluginOperationResult.PluginResultCode;
import org.syncany.util.StringUtil;

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

import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;

public class PluginCommand extends Command {
	private boolean minimalOutput = false;

	@Override
	public CommandScope getRequiredCommandScope() {
		return CommandScope.ANY;
	}

	@Override
	public boolean canExecuteInDaemonScope() {
		return false; // TODO [low] Doesn't have an impact if command scope is ANY
	}

	@Override
	public int execute(String[] operationArgs) throws Exception {
		PluginOperationOptions operationOptions = parseOptions(operationArgs);
		PluginOperationResult operationResult = new PluginOperation(config, operationOptions).execute();

		printResults(operationResult);

		return 0;
	}

	@Override
	public PluginOperationOptions parseOptions(String[] operationArgs) throws Exception {
		PluginOperationOptions operationOptions = new PluginOperationOptions();

		OptionParser parser = new OptionParser();
		OptionSpec<Void> optionLocal = parser.acceptsAll(asList("L", "local-only"));
		OptionSpec<Void> optionRemote = parser.acceptsAll(asList("R", "remote-only"));
		OptionSpec<Void> optionSnapshots = parser.acceptsAll(asList("s", "snapshot", "snapshots"));
		OptionSpec<Void> optionMinimalOutput = parser.acceptsAll(asList("m", "minimal-output"));
		OptionSpec<String> optionApiEndpoint = parser.acceptsAll(asList("a", "api-endpoint")).withRequiredArg();

		OptionSet options = parser.parse(operationArgs);

		// Files
		List<?> nonOptionArgs = options.nonOptionArguments();

		if (nonOptionArgs.size() == 0) {
			throw new Exception("Invalid syntax, please specify an action (list, install, remove, update).");
		}

		// <action>
		String actionStr = nonOptionArgs.get(0).toString();
		PluginOperationAction action = parsePluginAction(actionStr);

		operationOptions.setAction(action);

		// --minimal-output
		minimalOutput = options.has(optionMinimalOutput);

		// --snapshots
		operationOptions.setSnapshots(options.has(optionSnapshots));
		
		// --api-endpoint
		if (options.has(optionApiEndpoint)) {
			operationOptions.setApiEndpoint(options.valueOf(optionApiEndpoint));
		}

		// install|remove <plugin-id>
		if (action == PluginOperationAction.INSTALL || action == PluginOperationAction.REMOVE) {
			if (nonOptionArgs.size() != 2) {
				throw new Exception("Invalid syntax, please specify a plugin ID.");
			}

			// <plugin-id>
			String pluginId = nonOptionArgs.get(1).toString();
			operationOptions.setPluginId(pluginId);
		}

		// --local-only, --remote-only
		else if (action == PluginOperationAction.LIST) {
			if (options.has(optionLocal)) {
				operationOptions.setListMode(PluginListMode.LOCAL);
			}
			else if (options.has(optionRemote)) {
				operationOptions.setListMode(PluginListMode.REMOTE);
			}
			else {
				operationOptions.setListMode(PluginListMode.ALL);
			}

			// <plugin-id> (optional in 'list' or 'update')
			if (nonOptionArgs.size() == 2) {
				String pluginId = nonOptionArgs.get(1).toString();
				operationOptions.setPluginId(pluginId);
			}
		}

		else if (action == PluginOperationAction.UPDATE && nonOptionArgs.size() == 2) {
			String pluginId = nonOptionArgs.get(1).toString();
			operationOptions.setPluginId(pluginId);
		}

		return operationOptions;
	}

	private PluginOperationAction parsePluginAction(String actionStr) throws Exception {
		try {
			return PluginOperationAction.valueOf(actionStr.toUpperCase());
		}
		catch (Exception e) {
			throw new Exception("Invalid syntax, unknown action '" + actionStr + "'");
		}
	}

	@Override
	public void printResults(OperationResult operationResult) {
		PluginOperationResult concreteOperationResult = (PluginOperationResult) operationResult;

		switch (concreteOperationResult.getAction()) {
			case LIST:
				printResultList(concreteOperationResult);
				return;

			case INSTALL:
				printResultInstall(concreteOperationResult);
				return;

			case REMOVE:
				printResultRemove(concreteOperationResult);
				return;

			case UPDATE:
				printResultUpdate(concreteOperationResult);
				return;

			default:
				out.println("Unknown action: " + concreteOperationResult.getAction());
		}
	}

	private void printResultList(PluginOperationResult operationResult) {
		if (operationResult.getResultCode() == PluginResultCode.OK) {
			List<String[]> tableValues = new ArrayList<String[]>();
			
			tableValues.add(new String[]{"Id", "Name", "Local Version", "Type", "Remote Version", "Updatable", "Provided By"});
			
			int outdatedCount = 0;
			int updatableCount = 0;
			int thirdPartyCount = 0;

			for (ExtendedPluginInfo extPluginInfo : operationResult.getPluginList()) {
				PluginInfo pluginInfo = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo() : extPluginInfo.getRemotePluginInfo();

				String localVersionStr = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo().getPluginVersion() : "";
				String installedStr = extPluginInfo.isInstalled() ? (extPluginInfo.canUninstall() ? "User" : "Global") : "";
				String remoteVersionStr = (extPluginInfo.isRemoteAvailable()) ? extPluginInfo.getRemotePluginInfo().getPluginVersion() : "";
				String thirdPartyStr = (pluginInfo.isPluginThirdParty()) ? "Third Party" : "Syncany Team"; 
				String updatableStr = "";

				if (extPluginInfo.isInstalled() && extPluginInfo.isOutdated()) {
					if (extPluginInfo.canUninstall()) {
						updatableStr = "Auto";
						updatableCount++;
					}
					else {
						updatableStr = "Manual";
					}

					outdatedCount++;
				}

				if (pluginInfo.isPluginThirdParty()) {
					thirdPartyCount++;
				}
				
				tableValues.add(new String[]{pluginInfo.getPluginId(), pluginInfo.getPluginName(), localVersionStr, installedStr, remoteVersionStr, updatableStr, thirdPartyStr});
			}

			CliTableUtil.printTable(out, tableValues, "No plugins found.");

			if (outdatedCount > 0) {
				String isAre = (outdatedCount == 1) ? "is" : "are";
				String pluginPlugins = (outdatedCount == 1) ? "plugin" : "plugins";
				
				out.printf("\nUpdates:\nThere %s %d outdated %s, %d of them %s automatically updatable.\n", isAre, outdatedCount, pluginPlugins, updatableCount, isAre);
			}
			
			if (thirdPartyCount > 0) {
				String pluginPlugins = (thirdPartyCount == 1) ? "plugin" : "plugins";				
				out.printf("\nThird party plugins:\nPlease note that the Syncany Team does not review or maintain the third-party %s\nlisted above. Please report issues to the corresponding plugin site.\n", pluginPlugins);
			}
		}
		else {
			out.printf("Listing plugins failed. No connection? Try -d to get more details.\n");
			out.println();
		}
	}

	private void printResultInstall(PluginOperationResult operationResult) {
		// Print minimal result
		if (minimalOutput) {
			if (operationResult.getResultCode() == PluginResultCode.OK) {
				out.println("OK");
			}
			else {
				out.println("NOK");
			}
		}
		// Print regular result
		else {
			if (operationResult.getResultCode() == PluginResultCode.OK) {
				out.printf("Plugin successfully installed from %s\n", operationResult.getSourcePluginPath());
				out.printf("Install location: %s\n", operationResult.getTargetPluginPath());
				out.println();

				printPluginDetails(operationResult.getAffectedPluginInfo());
				printPluginConflictWarning(operationResult);
			}
			else {
				out.println("Plugin installation failed. Try -d to get more details.");
				out.println();
			}
		}
	}

	private void printResultUpdate(final PluginOperationResult operationResult) {
		// Print regular result
		if (operationResult.getResultCode() == PluginResultCode.OK) {
			if (operationResult.getUpdatedPluginIds().size() == 0) {
				out.println("All plugins are up to date.");
			}
			else {
				Iterables.removeAll(operationResult.getUpdatedPluginIds(), operationResult.getErroneousPluginIds());
				Iterables.removeAll(operationResult.getUpdatedPluginIds(), operationResult.getDelayedPluginIds());

				if (operationResult.getDelayedPluginIds().size() > 0) {
					out.printf("Plugins to be updated: %s\n", StringUtil.join(operationResult.getDelayedPluginIds(), ", "));
				}

				if (operationResult.getUpdatedPluginIds().size() > 0) {
					out.printf("Plugins successfully updated: %s\n", StringUtil.join(operationResult.getUpdatedPluginIds(), ", "));
				}

				if (operationResult.getErroneousPluginIds().size() > 0) {
					out.printf("Failed to update %s. Try -d to get more details\n", StringUtil.join(operationResult.getErroneousPluginIds(), ", "));
				}

				out.println();
			}
		}
		else {
			out.println("Plugin update failed. Try -d to get more details.");
			out.println();
		}
	}

	private void printPluginConflictWarning(PluginOperationResult operationResult) {
		List<String> conflictingPluginIds = operationResult.getConflictingPluginIds();

		if (conflictingPluginIds != null && conflictingPluginIds.size() > 0) {
			out.println("---------------------------------------------------------------------------");
			out.printf(" WARNING: The installed plugin '%s' conflicts with other installed:\n", operationResult.getAffectedPluginInfo().getPluginId());
			out.printf("          plugin(s): %s\n", StringUtil.join(conflictingPluginIds, ", "));
			out.println();
			out.println(" If you'd like to use these plugins in the daemon, it is VERY likely");
			out.println(" that parts of the application WILL CRASH. Data corruption might occur!");
			out.println();
			out.println(" Using the plugins outside of the daemon (sy <command> ...) might also");
			out.println(" be an issue. Details about this in issue #154.");
			out.println("---------------------------------------------------------------------------");
			out.println();
		}
	}

	private void printResultRemove(PluginOperationResult operationResult) {
		// Print minimal result
		if (minimalOutput) {
			if (operationResult.getResultCode() == PluginResultCode.OK) {
				out.println("OK");
			}
			else {
				out.println("NOK");
			}
		}
		// Print regular result
		else {
			if (operationResult.getResultCode() == PluginResultCode.OK) {
				out.printf("Plugin successfully removed.\n");
				out.printf("Original local was %s\n", operationResult.getSourcePluginPath());
				out.println();
			}
			else {
				out.println("Plugin removal failed.");
				out.println();

				out.println("Note: Plugins shipped with the application or additional packages");
				out.println("      cannot be removed. These plugin are marked 'Global' in the list.");
				out.println();
			}
		}
	}

	private void printPluginDetails(PluginInfo pluginInfo) {
		out.println("Plugin details:");
		out.println("- ID: " + pluginInfo.getPluginId());
		out.println("- Name: " + pluginInfo.getPluginName());
		out.println("- Version: " + pluginInfo.getPluginVersion());
		out.println();
	}

	@Subscribe
	public void onConnectToHostEventReceived(ConnectToHostExternalEvent event) {
		if (!minimalOutput) {
			out.printr("Connecting to " + event.getHost() + " ...");
		}
	}

	@Subscribe
	public void onPluginInstallEventReceived(PluginInstallExternalEvent event) {
		if (!minimalOutput) {
			out.printr("Installing plugin from " + event.getSource() + " ...");
		}
	}
}