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.plugin;
019
020import java.io.BufferedReader;
021import java.io.BufferedWriter;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileOutputStream;
025import java.io.FileWriter;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.InputStreamReader;
029import java.io.PrintWriter;
030import java.net.URL;
031import java.net.URLConnection;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.List;
035import java.util.Map;
036import java.util.TreeMap;
037import java.util.jar.JarInputStream;
038import java.util.jar.Manifest;
039import java.util.logging.Level;
040import java.util.logging.Logger;
041
042import org.apache.commons.io.FileUtils;
043import org.apache.commons.io.IOUtils;
044import org.simpleframework.xml.core.Persister;
045import org.syncany.Client;
046import org.syncany.config.Config;
047import org.syncany.config.LocalEventBus;
048import org.syncany.config.UserConfig;
049import org.syncany.crypto.CipherUtil;
050import org.syncany.operations.Operation;
051import org.syncany.operations.daemon.messages.ConnectToHostExternalEvent;
052import org.syncany.operations.daemon.messages.PluginInstallExternalEvent;
053import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode;
054import org.syncany.operations.plugin.PluginOperationResult.PluginResultCode;
055import org.syncany.plugins.Plugin;
056import org.syncany.plugins.Plugins;
057import org.syncany.util.EnvironmentUtil;
058import org.syncany.util.FileUtil;
059import org.syncany.util.StringUtil;
060
061import com.github.zafarkhaja.semver.Version;
062import com.google.common.base.Function;
063import com.google.common.base.Predicate;
064import com.google.common.collect.Iterables;
065import com.google.common.collect.Lists;
066
067/**
068 * The plugin operation installs, removes and lists storage {@link Plugin}s.
069 *
070 * <p>The plugin implements these three functionalities as different
071 * {@link PluginOperationAction}:
072 *
073 * <ul>
074 * <li><code>INSTALL</code>: Installation means copying a file to the user plugin directory
075 * as specified by {@link UserConfig#getUserPluginLibDir()}. A plugin can be installed
076 * from a local JAR file, a URL (the operation downloads a JAR file), or the
077 * API host (the operation find the plugin using the 'list' action and downloads
078 * the JAR file).</li>
079 * <li><code>REMOVE</code>: Removal means deleting a JAR file from the user plugin
080 * directoryThis action. This action simply finds the responsible plugin JAR
081 * file and deletes it. Only JAR files inside the user plugin direcory can be
082 * deleted.</li>
083 * <li><code>LIST</code>: Listing refers to a local and a remote list. The locally installed
084 * plugins can be queried by {@link Plugins#list()}. These plugins' JAR files must be
085 * in the application's class path. Remotely available plugins are queried through the
086 * API.</li>
087 * </ul>
088 *
089 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
090 */
091public class PluginOperation extends Operation {
092        private static final Logger logger = Logger.getLogger(PluginOperation.class.getSimpleName());
093
094        private static final String API_DEFAULT_ENDPOINT_URL = "https://api.syncany.org/v3";
095        private static final String API_PLUGIN_LIST_REQUEST_FORMAT = "%s/plugins/list?appVersion=%s&snapshots=%s&pluginId=%s&os=%s&arch=%s";
096
097        private static final String PURGEFILE_FILENAME = "purgefile";
098        private static final String UPDATE_FILENAME = "updatefile";
099
100        private PluginOperationOptions options;
101        private PluginOperationResult result;
102
103        private LocalEventBus eventBus;
104
105        public PluginOperation(Config config, PluginOperationOptions options) {
106                super(config);
107
108                this.options = options;
109                this.result = new PluginOperationResult();
110
111                this.eventBus = LocalEventBus.getInstance();
112        }
113
114        @Override
115        public PluginOperationResult execute() throws Exception {
116                result.setAction(options.getAction());
117
118                switch (options.getAction()) {
119                        case LIST:
120                                return executeList();
121
122                        case INSTALL:
123                                return executeInstall();
124
125                        case REMOVE:
126                                return executeRemove();
127
128                        case UPDATE:
129                                return executeUpdate();
130
131                        default:
132                                throw new Exception("Unknown action: " + options.getAction());
133                }
134        }
135
136        private PluginOperationResult executeUpdate() throws Exception {
137                List<String> updateablePlugins = findUpdateCandidates();
138                List<String> erroneousPlugins = Lists.newArrayList();
139                List<String> delayedPlugins = Lists.newArrayList();
140
141                // update only a specific plugin if it is updatable and provided
142                String forcePluginId = options.getPluginId();
143                logger.log(Level.FINE, "Force plugin is " + forcePluginId);
144                if (forcePluginId != null) {
145                        if (updateablePlugins.contains(forcePluginId)) {
146                                updateablePlugins = Lists.newArrayList(forcePluginId);
147                        }
148                        else {
149                                logger.log(Level.WARNING, "User requested to update a non-updatable plugin: " + forcePluginId);
150                                erroneousPlugins.add(forcePluginId);
151                                updateablePlugins = Lists.newArrayList(); // empty list
152                        }
153                }
154
155                logger.log(Level.INFO, "The following plugins can be automatically updated: " + StringUtil.join(updateablePlugins, ", "));
156
157                for (String pluginId : updateablePlugins) {
158                        // first remove
159                        PluginOperationResult removeResult = executeRemove(pluginId);
160
161                        if (removeResult.getResultCode() == PluginResultCode.NOK) {
162                                logger.log(Level.SEVERE, "Unable to remove " + pluginId + " during the update process");
163                                erroneousPlugins.add(pluginId);
164                                continue;
165                        }
166
167                        // ... and install again
168                        if (EnvironmentUtil.isWindows()) {
169                                logger.log(Level.FINE, "Appending jar to updatefile");
170                                File updatefilePath = new File(UserConfig.getUserConfigDir(), UPDATE_FILENAME);
171
172                                try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(updatefilePath, true)))) {
173                                        out.println(pluginId + (options.isSnapshots() ?  " --snapshot" : ""));
174                                        delayedPlugins.add(pluginId);
175                                }
176                                catch (IOException e) {
177                                        logger.log(Level.SEVERE, "Unable to append to updatefile " + updatefilePath, e);
178                                        erroneousPlugins.add(pluginId);
179                                }
180                        }
181                        else {
182                                PluginOperationResult installResult = executeInstallFromApiHost(pluginId);
183
184                                if (installResult.getResultCode() == PluginResultCode.NOK) {
185                                        logger.log(Level.SEVERE, "Unable to install " + pluginId + " during the update process");
186                                        erroneousPlugins.add(pluginId);
187                                }
188                        }
189                }
190
191                if (erroneousPlugins.size() > 0 && erroneousPlugins.size() == updateablePlugins.size()) {
192                        result.setResultCode(PluginResultCode.NOK);
193                }
194                else {
195                        result.setResultCode(PluginResultCode.OK);
196                }
197
198                result.setUpdatedPluginIds(updateablePlugins);
199                result.setErroneousPluginIds(erroneousPlugins);
200                result.setDelayedPluginIds(delayedPlugins);
201
202                return result;
203        }
204
205        private List<String> findUpdateCandidates() throws Exception {
206                List<ExtendedPluginInfo> updateCandidates = executeList().getPluginList();
207
208                Iterables.removeIf(updateCandidates, new Predicate<ExtendedPluginInfo>() {
209                        @Override
210                        public boolean apply(ExtendedPluginInfo pluginInfo) {
211                                return !pluginInfo.isInstalled() || !pluginInfo.canUninstall() || !pluginInfo.isOutdated();
212                        }
213                });
214
215                return Lists.transform(updateCandidates, new Function<ExtendedPluginInfo, String>() {
216                        @Override
217                        public String apply(ExtendedPluginInfo pluginInfo) {
218                                return pluginInfo.getLocalPluginInfo().getPluginId();
219                        }
220                });
221        }
222
223        private PluginOperationResult executeRemove() throws Exception {
224                return executeRemove(options.getPluginId());
225        }
226
227        private PluginOperationResult executeRemove(String pluginId) throws Exception {
228                Plugin plugin = Plugins.get(pluginId);
229
230                if (plugin == null) {
231                        throw new Exception("Plugin not installed.");
232                }
233
234                File pluginJarFile = getJarFile(plugin);
235                boolean canUninstall = canUninstall(pluginJarFile);
236
237                if (canUninstall) {
238                        PluginInfo pluginInfo = readPluginInfoFromJar(pluginJarFile);
239
240                        logger.log(Level.INFO, "Uninstalling plugin from file " + pluginJarFile);
241                        boolean deleted = pluginJarFile.delete();
242
243                        // JAR files are locked on Windows, adding JAR filename to a list for delayed deletion (by batch file)
244                        if (EnvironmentUtil.isWindows() || !deleted) {
245                                logger.log(Level.FINE, "Appending jar to purgefile (" + EnvironmentUtil.isWindows() + ", "+ deleted +")");
246                                File purgefilePath = new File(UserConfig.getUserConfigDir(), PURGEFILE_FILENAME);
247
248                                try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(purgefilePath, true)))) {
249                                        out.println(pluginJarFile.getAbsolutePath());
250                                }
251                                catch (IOException e) {
252                                        logger.log(Level.SEVERE, "Unable to append to purgefile " + purgefilePath, e);
253                                }
254                        }
255
256                        // refresh plugin list
257                        Plugins.refresh();
258
259                        result.setSourcePluginPath(pluginJarFile.getAbsolutePath());
260                        result.setAffectedPluginInfo(pluginInfo);
261                        result.setResultCode(PluginResultCode.OK);
262                }
263                else {
264                        logger.log(Level.INFO, "Plugin can NOT be uninstalled because class location not in " + UserConfig.getUserPluginLibDir());
265                        result.setResultCode(PluginResultCode.NOK);
266                }
267
268                return result;
269        }
270
271        private boolean canUninstall(File pluginJarFile) {
272                File globalUserPluginDir = UserConfig.getUserPluginLibDir();
273                return pluginJarFile != null && pluginJarFile.getAbsolutePath().startsWith(globalUserPluginDir.getAbsolutePath());
274        }
275
276        private File getJarFile(Plugin plugin) {
277                Class<? extends Plugin> pluginClass = plugin.getClass();
278                URL pluginClassLocation = pluginClass.getResource('/' + pluginClass.getName().replace('.', '/') + ".class");
279                String pluginClassLocationStr = pluginClassLocation.toString();
280
281                logger.log(Level.INFO, "Plugin class is at " + pluginClassLocationStr);
282
283                if (pluginClassLocationStr.startsWith("jar:file:")) {
284                        int indexStartAfterSchema = "jar:file:".length();
285                        int indexEndAtExclamationPoint = pluginClassLocationStr.indexOf("!");
286                        File pluginJarFile = new File(pluginClassLocationStr.substring(indexStartAfterSchema, indexEndAtExclamationPoint));
287
288                        logger.log(Level.INFO, "Plugin is in JAR at " + pluginJarFile);
289                        return pluginJarFile;
290                }
291                else {
292                        logger.log(Level.INFO, "Plugin is not in a JAR file. Probably in test environment.");
293                        return null;
294                }
295        }
296
297        private PluginOperationResult executeInstall() throws Exception {
298                String pluginId = options.getPluginId();
299                File potentialLocalPluginJarFile = new File(pluginId);
300
301                if (pluginId.matches("^https?://.+")) {
302                        return executeInstallFromUrl(pluginId);
303                }
304                else if (potentialLocalPluginJarFile.exists()) {
305                        return executeInstallFromLocalFile(potentialLocalPluginJarFile);
306                }
307                else {
308                        return executeInstallFromApiHost(pluginId);
309                }
310        }
311
312        private PluginOperationResult executeInstallFromApiHost(String pluginId) throws Exception {
313                checkPluginNotInstalled(pluginId);
314
315                PluginInfo pluginInfo = getRemotePluginInfo(pluginId);
316
317                if (pluginInfo == null) {
318                        throw new Exception("Plugin with ID '" + pluginId + "' not found");
319                }
320
321                checkPluginCompatibility(pluginInfo);
322
323                eventBus.post(new PluginInstallExternalEvent(pluginInfo.getDownloadUrl()));
324
325                File tempPluginJarFile = downloadPluginJar(pluginInfo.getDownloadUrl());
326                String expectedChecksum = pluginInfo.getSha256sum();
327                String actualChecksum = calculateChecksum(tempPluginJarFile);
328
329                if (expectedChecksum == null || !expectedChecksum.equals(actualChecksum)) {
330                        throw new Exception("Checksum mismatch. Expected: " + expectedChecksum + ", but was: " + actualChecksum);
331                }
332
333                logger.log(Level.INFO, "Plugin JAR checksum verified: " + actualChecksum);
334
335                File targetPluginJarFile = installPlugin(tempPluginJarFile, pluginInfo);
336
337                result.setSourcePluginPath(pluginInfo.getDownloadUrl());
338                result.setTargetPluginPath(targetPluginJarFile.getAbsolutePath());
339                result.setAffectedPluginInfo(pluginInfo);
340                result.setResultCode(PluginResultCode.OK);
341
342                return result;
343        }
344
345        private void checkPluginCompatibility(PluginInfo pluginInfo) throws Exception {
346                Version applicationVersion = Version.valueOf(Client.getApplicationVersion());
347                Version pluginAppMinVersion = Version.valueOf(pluginInfo.getPluginAppMinVersion());
348
349                logger.log(Level.INFO, "Checking plugin compatibility:");
350                logger.log(Level.INFO, "- Application version:             " + Client.getApplicationVersion() + "(" + applicationVersion + ")");
351                logger.log(Level.INFO, "- Plugin min. application version: " + pluginInfo.getPluginAppMinVersion() + "(" + pluginAppMinVersion + ")");
352
353                if (applicationVersion.lessThan(pluginAppMinVersion)) {
354                        throw new Exception("Plugin is incompatible to this application version. Plugin min. application version is "
355                                                        + pluginInfo.getPluginAppMinVersion() + ", current application version is " + Client.getApplicationVersion());
356                }
357
358                // Verify if any conflicting plugins are installed
359                logger.log(Level.INFO, "Checking for conflicting plugins.");
360
361                List<String> conflictingIds = pluginInfo.getConflictingPluginIds();
362                List<String> conflictingInstalledIds = new ArrayList<String>();
363
364                if (conflictingIds != null) {
365                        for (String pluginId : conflictingIds) {
366                                Plugin plugin = Plugins.get(pluginId);
367
368                                if (plugin != null) {
369                                        logger.log(Level.INFO, "- Conflicting plugin " + pluginId + " found.");
370                                        conflictingInstalledIds.add(pluginId);
371                                }
372
373                                logger.log(Level.FINE, "- Conflicting plugin " + pluginId + " not installed");
374                        }
375                }
376
377                result.setConflictingPlugins(conflictingInstalledIds);
378        }
379
380        private String calculateChecksum(File tempPluginJarFile) throws Exception {
381                CipherUtil.enableUnlimitedStrength();
382
383                byte[] actualChecksum = FileUtil.createChecksum(tempPluginJarFile, "SHA256");
384                return StringUtil.toHex(actualChecksum);
385        }
386
387        private PluginOperationResult executeInstallFromLocalFile(File pluginJarFile) throws Exception {
388                eventBus.post(new PluginInstallExternalEvent(pluginJarFile.getAbsolutePath()));
389
390                PluginInfo pluginInfo = readPluginInfoFromJar(pluginJarFile);
391
392                checkPluginNotInstalled(pluginInfo.getPluginId());
393                checkPluginCompatibility(pluginInfo);
394
395                File targetPluginJarFile = installPlugin(pluginJarFile, pluginInfo);
396
397                result.setSourcePluginPath(pluginJarFile.getPath());
398                result.setTargetPluginPath(targetPluginJarFile.getPath());
399                result.setAffectedPluginInfo(pluginInfo);
400                result.setResultCode(PluginResultCode.OK);
401
402                return result;
403        }
404
405        private PluginOperationResult executeInstallFromUrl(String downloadJarUrl) throws Exception {
406                eventBus.post(new PluginInstallExternalEvent(downloadJarUrl));
407
408                File tempPluginJarFile = downloadPluginJar(downloadJarUrl);
409                PluginInfo pluginInfo = readPluginInfoFromJar(tempPluginJarFile);
410
411                checkPluginNotInstalled(pluginInfo.getPluginId());
412                checkPluginCompatibility(pluginInfo);
413
414                File targetPluginJarFile = installPlugin(tempPluginJarFile, pluginInfo);
415
416                result.setSourcePluginPath(downloadJarUrl);
417                result.setTargetPluginPath(targetPluginJarFile.getPath());
418                result.setAffectedPluginInfo(pluginInfo);
419                result.setResultCode(PluginResultCode.OK);
420
421                return result;
422        }
423
424        private void checkPluginNotInstalled(String pluginId) throws Exception {
425                Plugin locallyInstalledPlugin = Plugins.get(pluginId);
426
427                if (locallyInstalledPlugin != null) {
428                        throw new Exception("Plugin '" + pluginId + "' already installed. Use 'sy plugin remove " + pluginId + "' to uninstall it first.");
429                }
430
431                logger.log(Level.INFO, "Plugin '" + pluginId + "' not installed. Okay!");
432        }
433
434        private PluginInfo readPluginInfoFromJar(File pluginJarFile) throws Exception {
435                try (JarInputStream jarStream = new JarInputStream(new FileInputStream(pluginJarFile))) {
436                        Manifest jarManifest = jarStream.getManifest();
437
438                        if (jarManifest == null) {
439                                throw new Exception("Given file is not a valid Syncany plugin file (not a JAR file, or no manifest).");
440                        }
441
442                        String pluginId = jarManifest.getMainAttributes().getValue("Plugin-Id");
443
444                        if (pluginId == null) {
445                                throw new Exception("Given file is not a valid Syncany plugin file (no plugin ID in manifest).");
446                        }
447
448                        PluginInfo pluginInfo = new PluginInfo();
449
450                        pluginInfo.setPluginId(pluginId);
451                        pluginInfo.setPluginName(jarManifest.getMainAttributes().getValue("Plugin-Name"));
452                        pluginInfo.setPluginVersion(jarManifest.getMainAttributes().getValue("Plugin-Version"));
453                        pluginInfo.setPluginDate(jarManifest.getMainAttributes().getValue("Plugin-Date"));
454                        pluginInfo.setPluginAppMinVersion(jarManifest.getMainAttributes().getValue("Plugin-App-Min-Version"));
455                        pluginInfo.setPluginRelease(Boolean.parseBoolean(jarManifest.getMainAttributes().getValue("Plugin-Release")));
456
457                        if (jarManifest.getMainAttributes().getValue("Plugin-Conflicts-With") != null) {
458                                pluginInfo.setConflictingPluginIds(Arrays.asList(jarManifest.getMainAttributes().getValue("Plugin-Conflicts-With")));
459                        }
460
461                        return pluginInfo;
462                }
463        }
464
465        private File installPlugin(File pluginJarFile, PluginInfo pluginInfo) throws IOException {
466                File globalUserPluginDir = UserConfig.getUserPluginLibDir();
467                globalUserPluginDir.mkdirs();
468
469                File targetPluginJarFile = new File(globalUserPluginDir, String.format("syncany-plugin-%s-%s.jar", pluginInfo.getPluginId(),
470                                                pluginInfo.getPluginVersion()));
471
472                logger.log(Level.INFO, "Installing plugin from " + pluginJarFile + " to " + targetPluginJarFile + " ...");
473                FileUtils.copyFile(pluginJarFile, targetPluginJarFile);
474
475                return targetPluginJarFile;
476        }
477
478        /**
479         * Downloads the plugin JAR from the given URL to a temporary
480         * local location.
481         */
482        private File downloadPluginJar(String pluginJarUrl) throws Exception {
483                URL pluginJarFile = new URL(pluginJarUrl);
484                logger.log(Level.INFO, "Querying " + pluginJarFile + " ...");
485
486                URLConnection urlConnection = pluginJarFile.openConnection();
487                urlConnection.setConnectTimeout(2000);
488                urlConnection.setReadTimeout(2000);
489
490                File tempPluginFile = File.createTempFile("syncany-plugin", "tmp");
491                tempPluginFile.deleteOnExit();
492
493                logger.log(Level.INFO, "Downloading to " + tempPluginFile + " ...");
494                FileOutputStream tempPluginFileOutputStream = new FileOutputStream(tempPluginFile);
495                InputStream remoteJarFileInputStream = urlConnection.getInputStream();
496
497                IOUtils.copy(remoteJarFileInputStream, tempPluginFileOutputStream);
498
499                remoteJarFileInputStream.close();
500                tempPluginFileOutputStream.close();
501
502                if (!tempPluginFile.exists() || tempPluginFile.length() == 0) {
503                        throw new Exception("Downloading plugin file failed, URL was " + pluginJarUrl);
504                }
505
506                return tempPluginFile;
507        }
508
509        private PluginOperationResult executeList() throws Exception {
510                final Version applicationVersion = Version.valueOf(Client.getApplicationVersion());
511                Map<String, ExtendedPluginInfo> pluginInfos = new TreeMap<String, ExtendedPluginInfo>();
512
513                // First, list local plugins
514                if (options.getListMode() == PluginListMode.ALL || options.getListMode() == PluginListMode.LOCAL) {
515                        for (PluginInfo localPluginInfo : getLocalList()) {
516                                if (options.getPluginId() != null && !localPluginInfo.getPluginId().equals(options.getPluginId())) {
517                                        continue;
518                                }
519
520                                // Determine standard plugin information
521                                ExtendedPluginInfo extendedPluginInfo = new ExtendedPluginInfo();
522
523                                extendedPluginInfo.setLocalPluginInfo(localPluginInfo);
524                                extendedPluginInfo.setInstalled(true);
525
526                                // Test if plugin can be uninstalled
527                                Plugin plugin = Plugins.get(localPluginInfo.getPluginId());
528                                File pluginJarFile = getJarFile(plugin);
529                                boolean canUninstall = canUninstall(pluginJarFile);
530
531                                extendedPluginInfo.setCanUninstall(canUninstall);
532
533                                // Add to list
534                                pluginInfos.put(localPluginInfo.getPluginId(), extendedPluginInfo);
535                        }
536                }
537
538                // Then, list remote plugins
539                if (options.getListMode() == PluginListMode.ALL || options.getListMode() == PluginListMode.REMOTE) {
540                        for (PluginInfo remotePluginInfo : getRemotePluginInfoList()) {
541                                if (options.getPluginId() != null && !remotePluginInfo.getPluginId().equals(options.getPluginId())) {
542                                        continue;
543                                }
544
545                                ExtendedPluginInfo extendedPluginInfo = pluginInfos.get(remotePluginInfo.getPluginId());
546                                boolean localPluginInstalled = extendedPluginInfo != null;
547
548                                if (!localPluginInstalled) { // Locally not installed
549                                        extendedPluginInfo = new ExtendedPluginInfo();
550
551                                        extendedPluginInfo.setInstalled(false);
552                                        extendedPluginInfo.setRemoteAvailable(true);
553                                }
554                                else { // Locally also installed
555                                        extendedPluginInfo.setRemoteAvailable(true);
556
557                                        Version localVersion = Version.valueOf(extendedPluginInfo.getLocalPluginInfo().getPluginVersion());
558                                        Version remoteVersion = Version.valueOf(remotePluginInfo.getPluginVersion());
559                                        Version remoteMinAppVersion = Version.valueOf(remotePluginInfo.getPluginAppMinVersion());
560
561                                        boolean localVersionOutdated = localVersion.lessThan(remoteVersion);
562                                        boolean applicationVersionCompatible = applicationVersion.greaterThanOrEqualTo(remoteMinAppVersion);
563                                        boolean pluginIsOutdated = localVersionOutdated && applicationVersionCompatible;
564
565                                        extendedPluginInfo.setOutdated(pluginIsOutdated);
566                                }
567
568                                extendedPluginInfo.setRemotePluginInfo(remotePluginInfo);
569                                pluginInfos.put(remotePluginInfo.getPluginId(), extendedPluginInfo);
570                        }
571                }
572
573                result.setPluginList(new ArrayList<ExtendedPluginInfo>(pluginInfos.values()));
574                result.setResultCode(PluginResultCode.OK);
575
576                return result;
577        }
578
579        private List<PluginInfo> getLocalList() {
580                List<PluginInfo> localPluginInfos = new ArrayList<PluginInfo>();
581
582                for (Plugin plugin : Plugins.list()) {
583                        PluginInfo pluginInfo = new PluginInfo();
584
585                        pluginInfo.setPluginId(plugin.getId());
586                        pluginInfo.setPluginName(plugin.getName());
587                        pluginInfo.setPluginVersion(plugin.getVersion());
588
589                        localPluginInfos.add(pluginInfo);
590                }
591
592                return localPluginInfos;
593        }
594
595        private List<PluginInfo> getRemotePluginInfoList() throws Exception {
596                String remoteListStr = getRemoteListStr(null);
597                PluginListResponse pluginListResponse = new Persister().read(PluginListResponse.class, remoteListStr);
598
599                return pluginListResponse.getPlugins();
600        }
601
602        private PluginInfo getRemotePluginInfo(String pluginId) throws Exception {
603                String remoteListStr = getRemoteListStr(pluginId);
604                PluginListResponse pluginListResponse = new Persister().read(PluginListResponse.class, remoteListStr);
605
606                if (pluginListResponse.getPlugins().size() > 0) {
607                        return pluginListResponse.getPlugins().get(0);
608                }
609                else {
610                        return null;
611                }
612        }
613
614        private String getRemoteListStr(String pluginId) throws Exception {
615                String appVersion = Client.getApplicationVersion();
616                String snapshotsEnabled = (options.isSnapshots()) ? "true" : "false";
617                String pluginIdQueryStr = (pluginId != null) ? pluginId : "";
618                String osStr = EnvironmentUtil.getOperatingSystemDescription();
619                String archStr = EnvironmentUtil.getArchDescription();
620
621                String apiEndpointUrl = (options.getApiEndpoint() != null) ? options.getApiEndpoint() : API_DEFAULT_ENDPOINT_URL;
622                URL pluginListUrl = new URL(String.format(API_PLUGIN_LIST_REQUEST_FORMAT, apiEndpointUrl, appVersion, snapshotsEnabled, pluginIdQueryStr, osStr, archStr));
623
624                logger.log(Level.INFO, "Querying " + pluginListUrl + " ...");
625                eventBus.post(new ConnectToHostExternalEvent(pluginListUrl.getHost()));
626
627                URLConnection urlConnection = pluginListUrl.openConnection();
628                urlConnection.setConnectTimeout(2000);
629                urlConnection.setReadTimeout(2000);
630
631                BufferedReader urlStreamReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
632                StringBuilder responseStringBuilder = new StringBuilder();
633
634                String line;
635                while ((line = urlStreamReader.readLine()) != null) {
636                        responseStringBuilder.append(line);
637                }
638
639                String responseStr = responseStringBuilder.toString();
640                logger.log(Level.INFO, "Response from api.syncany.org: " + responseStr);
641
642                return responseStr;
643        }
644}