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.cli;
019
020import static java.util.Arrays.asList;
021
022import java.util.ArrayList;
023import java.util.List;
024
025import org.syncany.cli.util.CliTableUtil;
026import org.syncany.operations.OperationResult;
027import org.syncany.operations.daemon.messages.ConnectToHostExternalEvent;
028import org.syncany.operations.daemon.messages.PluginInstallExternalEvent;
029import org.syncany.operations.plugin.ExtendedPluginInfo;
030import org.syncany.operations.plugin.PluginInfo;
031import org.syncany.operations.plugin.PluginOperation;
032import org.syncany.operations.plugin.PluginOperationAction;
033import org.syncany.operations.plugin.PluginOperationOptions;
034import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode;
035import org.syncany.operations.plugin.PluginOperationResult;
036import org.syncany.operations.plugin.PluginOperationResult.PluginResultCode;
037import org.syncany.util.StringUtil;
038
039import com.google.common.collect.Iterables;
040import com.google.common.eventbus.Subscribe;
041
042import joptsimple.OptionParser;
043import joptsimple.OptionSet;
044import joptsimple.OptionSpec;
045
046public class PluginCommand extends Command {
047        private boolean minimalOutput = false;
048
049        @Override
050        public CommandScope getRequiredCommandScope() {
051                return CommandScope.ANY;
052        }
053
054        @Override
055        public boolean canExecuteInDaemonScope() {
056                return false; // TODO [low] Doesn't have an impact if command scope is ANY
057        }
058
059        @Override
060        public int execute(String[] operationArgs) throws Exception {
061                PluginOperationOptions operationOptions = parseOptions(operationArgs);
062                PluginOperationResult operationResult = new PluginOperation(config, operationOptions).execute();
063
064                printResults(operationResult);
065
066                return 0;
067        }
068
069        @Override
070        public PluginOperationOptions parseOptions(String[] operationArgs) throws Exception {
071                PluginOperationOptions operationOptions = new PluginOperationOptions();
072
073                OptionParser parser = new OptionParser();
074                OptionSpec<Void> optionLocal = parser.acceptsAll(asList("L", "local-only"));
075                OptionSpec<Void> optionRemote = parser.acceptsAll(asList("R", "remote-only"));
076                OptionSpec<Void> optionSnapshots = parser.acceptsAll(asList("s", "snapshot", "snapshots"));
077                OptionSpec<Void> optionMinimalOutput = parser.acceptsAll(asList("m", "minimal-output"));
078                OptionSpec<String> optionApiEndpoint = parser.acceptsAll(asList("a", "api-endpoint")).withRequiredArg();
079
080                OptionSet options = parser.parse(operationArgs);
081
082                // Files
083                List<?> nonOptionArgs = options.nonOptionArguments();
084
085                if (nonOptionArgs.size() == 0) {
086                        throw new Exception("Invalid syntax, please specify an action (list, install, remove, update).");
087                }
088
089                // <action>
090                String actionStr = nonOptionArgs.get(0).toString();
091                PluginOperationAction action = parsePluginAction(actionStr);
092
093                operationOptions.setAction(action);
094
095                // --minimal-output
096                minimalOutput = options.has(optionMinimalOutput);
097
098                // --snapshots
099                operationOptions.setSnapshots(options.has(optionSnapshots));
100                
101                // --api-endpoint
102                if (options.has(optionApiEndpoint)) {
103                        operationOptions.setApiEndpoint(options.valueOf(optionApiEndpoint));
104                }
105
106                // install|remove <plugin-id>
107                if (action == PluginOperationAction.INSTALL || action == PluginOperationAction.REMOVE) {
108                        if (nonOptionArgs.size() != 2) {
109                                throw new Exception("Invalid syntax, please specify a plugin ID.");
110                        }
111
112                        // <plugin-id>
113                        String pluginId = nonOptionArgs.get(1).toString();
114                        operationOptions.setPluginId(pluginId);
115                }
116
117                // --local-only, --remote-only
118                else if (action == PluginOperationAction.LIST) {
119                        if (options.has(optionLocal)) {
120                                operationOptions.setListMode(PluginListMode.LOCAL);
121                        }
122                        else if (options.has(optionRemote)) {
123                                operationOptions.setListMode(PluginListMode.REMOTE);
124                        }
125                        else {
126                                operationOptions.setListMode(PluginListMode.ALL);
127                        }
128
129                        // <plugin-id> (optional in 'list' or 'update')
130                        if (nonOptionArgs.size() == 2) {
131                                String pluginId = nonOptionArgs.get(1).toString();
132                                operationOptions.setPluginId(pluginId);
133                        }
134                }
135
136                else if (action == PluginOperationAction.UPDATE && nonOptionArgs.size() == 2) {
137                        String pluginId = nonOptionArgs.get(1).toString();
138                        operationOptions.setPluginId(pluginId);
139                }
140
141                return operationOptions;
142        }
143
144        private PluginOperationAction parsePluginAction(String actionStr) throws Exception {
145                try {
146                        return PluginOperationAction.valueOf(actionStr.toUpperCase());
147                }
148                catch (Exception e) {
149                        throw new Exception("Invalid syntax, unknown action '" + actionStr + "'");
150                }
151        }
152
153        @Override
154        public void printResults(OperationResult operationResult) {
155                PluginOperationResult concreteOperationResult = (PluginOperationResult) operationResult;
156
157                switch (concreteOperationResult.getAction()) {
158                        case LIST:
159                                printResultList(concreteOperationResult);
160                                return;
161
162                        case INSTALL:
163                                printResultInstall(concreteOperationResult);
164                                return;
165
166                        case REMOVE:
167                                printResultRemove(concreteOperationResult);
168                                return;
169
170                        case UPDATE:
171                                printResultUpdate(concreteOperationResult);
172                                return;
173
174                        default:
175                                out.println("Unknown action: " + concreteOperationResult.getAction());
176                }
177        }
178
179        private void printResultList(PluginOperationResult operationResult) {
180                if (operationResult.getResultCode() == PluginResultCode.OK) {
181                        List<String[]> tableValues = new ArrayList<String[]>();
182                        
183                        tableValues.add(new String[]{"Id", "Name", "Local Version", "Type", "Remote Version", "Updatable", "Provided By"});
184                        
185                        int outdatedCount = 0;
186                        int updatableCount = 0;
187                        int thirdPartyCount = 0;
188
189                        for (ExtendedPluginInfo extPluginInfo : operationResult.getPluginList()) {
190                                PluginInfo pluginInfo = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo() : extPluginInfo.getRemotePluginInfo();
191
192                                String localVersionStr = (extPluginInfo.isInstalled()) ? extPluginInfo.getLocalPluginInfo().getPluginVersion() : "";
193                                String installedStr = extPluginInfo.isInstalled() ? (extPluginInfo.canUninstall() ? "User" : "Global") : "";
194                                String remoteVersionStr = (extPluginInfo.isRemoteAvailable()) ? extPluginInfo.getRemotePluginInfo().getPluginVersion() : "";
195                                String thirdPartyStr = (pluginInfo.isPluginThirdParty()) ? "Third Party" : "Syncany Team"; 
196                                String updatableStr = "";
197
198                                if (extPluginInfo.isInstalled() && extPluginInfo.isOutdated()) {
199                                        if (extPluginInfo.canUninstall()) {
200                                                updatableStr = "Auto";
201                                                updatableCount++;
202                                        }
203                                        else {
204                                                updatableStr = "Manual";
205                                        }
206
207                                        outdatedCount++;
208                                }
209
210                                if (pluginInfo.isPluginThirdParty()) {
211                                        thirdPartyCount++;
212                                }
213                                
214                                tableValues.add(new String[]{pluginInfo.getPluginId(), pluginInfo.getPluginName(), localVersionStr, installedStr, remoteVersionStr, updatableStr, thirdPartyStr});
215                        }
216
217                        CliTableUtil.printTable(out, tableValues, "No plugins found.");
218
219                        if (outdatedCount > 0) {
220                                String isAre = (outdatedCount == 1) ? "is" : "are";
221                                String pluginPlugins = (outdatedCount == 1) ? "plugin" : "plugins";
222                                
223                                out.printf("\nUpdates:\nThere %s %d outdated %s, %d of them %s automatically updatable.\n", isAre, outdatedCount, pluginPlugins, updatableCount, isAre);
224                        }
225                        
226                        if (thirdPartyCount > 0) {
227                                String pluginPlugins = (thirdPartyCount == 1) ? "plugin" : "plugins";                           
228                                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);
229                        }
230                }
231                else {
232                        out.printf("Listing plugins failed. No connection? Try -d to get more details.\n");
233                        out.println();
234                }
235        }
236
237        private void printResultInstall(PluginOperationResult operationResult) {
238                // Print minimal result
239                if (minimalOutput) {
240                        if (operationResult.getResultCode() == PluginResultCode.OK) {
241                                out.println("OK");
242                        }
243                        else {
244                                out.println("NOK");
245                        }
246                }
247                // Print regular result
248                else {
249                        if (operationResult.getResultCode() == PluginResultCode.OK) {
250                                out.printf("Plugin successfully installed from %s\n", operationResult.getSourcePluginPath());
251                                out.printf("Install location: %s\n", operationResult.getTargetPluginPath());
252                                out.println();
253
254                                printPluginDetails(operationResult.getAffectedPluginInfo());
255                                printPluginConflictWarning(operationResult);
256                        }
257                        else {
258                                out.println("Plugin installation failed. Try -d to get more details.");
259                                out.println();
260                        }
261                }
262        }
263
264        private void printResultUpdate(final PluginOperationResult operationResult) {
265                // Print regular result
266                if (operationResult.getResultCode() == PluginResultCode.OK) {
267                        if (operationResult.getUpdatedPluginIds().size() == 0) {
268                                out.println("All plugins are up to date.");
269                        }
270                        else {
271                                Iterables.removeAll(operationResult.getUpdatedPluginIds(), operationResult.getErroneousPluginIds());
272                                Iterables.removeAll(operationResult.getUpdatedPluginIds(), operationResult.getDelayedPluginIds());
273
274                                if (operationResult.getDelayedPluginIds().size() > 0) {
275                                        out.printf("Plugins to be updated: %s\n", StringUtil.join(operationResult.getDelayedPluginIds(), ", "));
276                                }
277
278                                if (operationResult.getUpdatedPluginIds().size() > 0) {
279                                        out.printf("Plugins successfully updated: %s\n", StringUtil.join(operationResult.getUpdatedPluginIds(), ", "));
280                                }
281
282                                if (operationResult.getErroneousPluginIds().size() > 0) {
283                                        out.printf("Failed to update %s. Try -d to get more details\n", StringUtil.join(operationResult.getErroneousPluginIds(), ", "));
284                                }
285
286                                out.println();
287                        }
288                }
289                else {
290                        out.println("Plugin update failed. Try -d to get more details.");
291                        out.println();
292                }
293        }
294
295        private void printPluginConflictWarning(PluginOperationResult operationResult) {
296                List<String> conflictingPluginIds = operationResult.getConflictingPluginIds();
297
298                if (conflictingPluginIds != null && conflictingPluginIds.size() > 0) {
299                        out.println("---------------------------------------------------------------------------");
300                        out.printf(" WARNING: The installed plugin '%s' conflicts with other installed:\n", operationResult.getAffectedPluginInfo().getPluginId());
301                        out.printf("          plugin(s): %s\n", StringUtil.join(conflictingPluginIds, ", "));
302                        out.println();
303                        out.println(" If you'd like to use these plugins in the daemon, it is VERY likely");
304                        out.println(" that parts of the application WILL CRASH. Data corruption might occur!");
305                        out.println();
306                        out.println(" Using the plugins outside of the daemon (sy <command> ...) might also");
307                        out.println(" be an issue. Details about this in issue #154.");
308                        out.println("---------------------------------------------------------------------------");
309                        out.println();
310                }
311        }
312
313        private void printResultRemove(PluginOperationResult operationResult) {
314                // Print minimal result
315                if (minimalOutput) {
316                        if (operationResult.getResultCode() == PluginResultCode.OK) {
317                                out.println("OK");
318                        }
319                        else {
320                                out.println("NOK");
321                        }
322                }
323                // Print regular result
324                else {
325                        if (operationResult.getResultCode() == PluginResultCode.OK) {
326                                out.printf("Plugin successfully removed.\n");
327                                out.printf("Original local was %s\n", operationResult.getSourcePluginPath());
328                                out.println();
329                        }
330                        else {
331                                out.println("Plugin removal failed.");
332                                out.println();
333
334                                out.println("Note: Plugins shipped with the application or additional packages");
335                                out.println("      cannot be removed. These plugin are marked 'Global' in the list.");
336                                out.println();
337                        }
338                }
339        }
340
341        private void printPluginDetails(PluginInfo pluginInfo) {
342                out.println("Plugin details:");
343                out.println("- ID: " + pluginInfo.getPluginId());
344                out.println("- Name: " + pluginInfo.getPluginName());
345                out.println("- Version: " + pluginInfo.getPluginVersion());
346                out.println();
347        }
348
349        @Subscribe
350        public void onConnectToHostEventReceived(ConnectToHostExternalEvent event) {
351                if (!minimalOutput) {
352                        out.printr("Connecting to " + event.getHost() + " ...");
353                }
354        }
355
356        @Subscribe
357        public void onPluginInstallEventReceived(PluginInstallExternalEvent event) {
358                if (!minimalOutput) {
359                        out.printr("Installing plugin from " + event.getSource() + " ...");
360                }
361        }
362}