org.syncany.operations.plugin.PluginOperation.java Source code

Java tutorial

Introduction

Here is the source code for org.syncany.operations.plugin.PluginOperation.java

Source

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2015 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.plugin;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.simpleframework.xml.core.Persister;
import org.syncany.Client;
import org.syncany.config.Config;
import org.syncany.config.LocalEventBus;
import org.syncany.config.UserConfig;
import org.syncany.crypto.CipherUtil;
import org.syncany.operations.Operation;
import org.syncany.operations.daemon.messages.ConnectToHostExternalEvent;
import org.syncany.operations.daemon.messages.PluginInstallExternalEvent;
import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode;
import org.syncany.operations.plugin.PluginOperationResult.PluginResultCode;
import org.syncany.plugins.Plugin;
import org.syncany.plugins.Plugins;
import org.syncany.util.EnvironmentUtil;
import org.syncany.util.FileUtil;
import org.syncany.util.StringUtil;

import com.github.zafarkhaja.semver.Version;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

/**
 * The plugin operation installs, removes and lists storage {@link Plugin}s.
 *
 * <p>The plugin implements these three functionalities as different
 * {@link PluginOperationAction}:
 *
 * <ul>
 * <li><tt>INSTALL</tt>: Installation means copying a file to the user plugin directory
 * as specified by {@link Client#getUserPluginLibDir()}. A plugin can be installed
 * from a local JAR file, a URL (the operation downloads a JAR file), or the
 * API host (the operation find the plugin using the 'list' action and downloads
 * the JAR file).</li>
 * <li><tt>REMOVE</tt>: Removal means deleting a JAR file from the user plugin
 * directoryThis action. This action simply finds the responsible plugin JAR
 * file and deletes it. Only JAR files inside the user plugin direcory can be
 * deleted.</li>
 * <li><tt>LIST</tt>: Listing refers to a local and a remote list. The locally installed
 * plugins can be queried by {@link Plugins#list()}. These plugins' JAR files must be
 * in the application's class path. Remotely available plugins are queried through the
 * API.</li>
 * </ul>
 *
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 */
public class PluginOperation extends Operation {
    private static final Logger logger = Logger.getLogger(PluginOperation.class.getSimpleName());

    private static final String API_DEFAULT_ENDPOINT_URL = "https://api.syncany.org/v3";
    private static final String API_PLUGIN_LIST_REQUEST_FORMAT = "%s/plugins/list?appVersion=%s&snapshots=%s&pluginId=%s&os=%s&arch=%s";

    private static final String PURGEFILE_FILENAME = "purgefile";
    private static final String UPDATE_FILENAME = "updatefile";

    private PluginOperationOptions options;
    private PluginOperationResult result;

    private LocalEventBus eventBus;

    public PluginOperation(Config config, PluginOperationOptions options) {
        super(config);

        this.options = options;
        this.result = new PluginOperationResult();

        this.eventBus = LocalEventBus.getInstance();
    }

    @Override
    public PluginOperationResult execute() throws Exception {
        result.setAction(options.getAction());

        switch (options.getAction()) {
        case LIST:
            return executeList();

        case INSTALL:
            return executeInstall();

        case REMOVE:
            return executeRemove();

        case UPDATE:
            return executeUpdate();

        default:
            throw new Exception("Unknown action: " + options.getAction());
        }
    }

    private PluginOperationResult executeUpdate() throws Exception {
        List<String> updateablePlugins = findUpdateCandidates();
        List<String> erroneousPlugins = Lists.newArrayList();
        List<String> delayedPlugins = Lists.newArrayList();

        // update only a specific plugin if it is updatable and provided
        String forcePluginId = options.getPluginId();
        logger.log(Level.FINE, "Force plugin is " + forcePluginId);
        if (forcePluginId != null) {
            if (updateablePlugins.contains(forcePluginId)) {
                updateablePlugins = Lists.newArrayList(forcePluginId);
            } else {
                logger.log(Level.WARNING, "User requested to update a non-updatable plugin: " + forcePluginId);
                erroneousPlugins.add(forcePluginId);
                updateablePlugins = Lists.newArrayList(); // empty list
            }
        }

        logger.log(Level.INFO,
                "The following plugins can be automatically updated: " + StringUtil.join(updateablePlugins, ", "));

        for (String pluginId : updateablePlugins) {
            // first remove
            PluginOperationResult removeResult = executeRemove(pluginId);

            if (removeResult.getResultCode() == PluginResultCode.NOK) {
                logger.log(Level.SEVERE, "Unable to remove " + pluginId + " during the update process");
                erroneousPlugins.add(pluginId);
                continue;
            }

            // ... and install again
            if (EnvironmentUtil.isWindows()) {
                logger.log(Level.FINE, "Appending jar to updatefile");
                File updatefilePath = new File(UserConfig.getUserConfigDir(), UPDATE_FILENAME);

                try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(updatefilePath, true)))) {
                    out.println(pluginId + (options.isSnapshots() ? " --snapshot" : ""));
                    delayedPlugins.add(pluginId);
                } catch (IOException e) {
                    logger.log(Level.SEVERE, "Unable to append to updatefile " + updatefilePath, e);
                    erroneousPlugins.add(pluginId);
                }
            } else {
                PluginOperationResult installResult = executeInstallFromApiHost(pluginId);

                if (installResult.getResultCode() == PluginResultCode.NOK) {
                    logger.log(Level.SEVERE, "Unable to install " + pluginId + " during the update process");
                    erroneousPlugins.add(pluginId);
                }
            }
        }

        if (erroneousPlugins.size() > 0 && erroneousPlugins.size() == updateablePlugins.size()) {
            result.setResultCode(PluginResultCode.NOK);
        } else {
            result.setResultCode(PluginResultCode.OK);
        }

        result.setUpdatedPluginIds(updateablePlugins);
        result.setErroneousPluginIds(erroneousPlugins);
        result.setDelayedPluginIds(delayedPlugins);

        return result;
    }

    private List<String> findUpdateCandidates() throws Exception {
        List<ExtendedPluginInfo> updateCandidates = executeList().getPluginList();

        Iterables.removeIf(updateCandidates, new Predicate<ExtendedPluginInfo>() {
            @Override
            public boolean apply(ExtendedPluginInfo pluginInfo) {
                return !pluginInfo.isInstalled() || !pluginInfo.canUninstall() || !pluginInfo.isOutdated();
            }
        });

        return Lists.transform(updateCandidates, new Function<ExtendedPluginInfo, String>() {
            @Override
            public String apply(ExtendedPluginInfo pluginInfo) {
                return pluginInfo.getLocalPluginInfo().getPluginId();
            }
        });
    }

    private PluginOperationResult executeRemove() throws Exception {
        return executeRemove(options.getPluginId());
    }

    private PluginOperationResult executeRemove(String pluginId) throws Exception {
        Plugin plugin = Plugins.get(pluginId);

        if (plugin == null) {
            throw new Exception("Plugin not installed.");
        }

        File pluginJarFile = getJarFile(plugin);
        boolean canUninstall = canUninstall(pluginJarFile);

        if (canUninstall) {
            PluginInfo pluginInfo = readPluginInfoFromJar(pluginJarFile);

            logger.log(Level.INFO, "Uninstalling plugin from file " + pluginJarFile);
            boolean deleted = pluginJarFile.delete();

            // JAR files are locked on Windows, adding JAR filename to a list for delayed deletion (by batch file)
            if (EnvironmentUtil.isWindows() || !deleted) {
                logger.log(Level.FINE,
                        "Appending jar to purgefile (" + EnvironmentUtil.isWindows() + ", " + deleted + ")");
                File purgefilePath = new File(UserConfig.getUserConfigDir(), PURGEFILE_FILENAME);

                try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(purgefilePath, true)))) {
                    out.println(pluginJarFile.getAbsolutePath());
                } catch (IOException e) {
                    logger.log(Level.SEVERE, "Unable to append to purgefile " + purgefilePath, e);
                }
            }

            // refresh plugin list
            Plugins.refresh();

            result.setSourcePluginPath(pluginJarFile.getAbsolutePath());
            result.setAffectedPluginInfo(pluginInfo);
            result.setResultCode(PluginResultCode.OK);
        } else {
            logger.log(Level.INFO, "Plugin can NOT be uninstalled because class location not in "
                    + UserConfig.getUserPluginLibDir());
            result.setResultCode(PluginResultCode.NOK);
        }

        return result;
    }

    private boolean canUninstall(File pluginJarFile) {
        File globalUserPluginDir = UserConfig.getUserPluginLibDir();
        return pluginJarFile != null
                && pluginJarFile.getAbsolutePath().startsWith(globalUserPluginDir.getAbsolutePath());
    }

    private File getJarFile(Plugin plugin) {
        Class<? extends Plugin> pluginClass = plugin.getClass();
        URL pluginClassLocation = pluginClass.getResource('/' + pluginClass.getName().replace('.', '/') + ".class");
        String pluginClassLocationStr = pluginClassLocation.toString();

        logger.log(Level.INFO, "Plugin class is at " + pluginClassLocationStr);

        if (pluginClassLocationStr.startsWith("jar:file:")) {
            int indexStartAfterSchema = "jar:file:".length();
            int indexEndAtExclamationPoint = pluginClassLocationStr.indexOf("!");
            File pluginJarFile = new File(
                    pluginClassLocationStr.substring(indexStartAfterSchema, indexEndAtExclamationPoint));

            logger.log(Level.INFO, "Plugin is in JAR at " + pluginJarFile);
            return pluginJarFile;
        } else {
            logger.log(Level.INFO, "Plugin is not in a JAR file. Probably in test environment.");
            return null;
        }
    }

    private PluginOperationResult executeInstall() throws Exception {
        String pluginId = options.getPluginId();
        File potentialLocalPluginJarFile = new File(pluginId);

        if (pluginId.matches("^https?://.+")) {
            return executeInstallFromUrl(pluginId);
        } else if (potentialLocalPluginJarFile.exists()) {
            return executeInstallFromLocalFile(potentialLocalPluginJarFile);
        } else {
            return executeInstallFromApiHost(pluginId);
        }
    }

    private PluginOperationResult executeInstallFromApiHost(String pluginId) throws Exception {
        checkPluginNotInstalled(pluginId);

        PluginInfo pluginInfo = getRemotePluginInfo(pluginId);

        if (pluginInfo == null) {
            throw new Exception("Plugin with ID '" + pluginId + "' not found");
        }

        checkPluginCompatibility(pluginInfo);

        eventBus.post(new PluginInstallExternalEvent(pluginInfo.getDownloadUrl()));

        File tempPluginJarFile = downloadPluginJar(pluginInfo.getDownloadUrl());
        String expectedChecksum = pluginInfo.getSha256sum();
        String actualChecksum = calculateChecksum(tempPluginJarFile);

        if (expectedChecksum == null || !expectedChecksum.equals(actualChecksum)) {
            throw new Exception(
                    "Checksum mismatch. Expected: " + expectedChecksum + ", but was: " + actualChecksum);
        }

        logger.log(Level.INFO, "Plugin JAR checksum verified: " + actualChecksum);

        File targetPluginJarFile = installPlugin(tempPluginJarFile, pluginInfo);

        result.setSourcePluginPath(pluginInfo.getDownloadUrl());
        result.setTargetPluginPath(targetPluginJarFile.getAbsolutePath());
        result.setAffectedPluginInfo(pluginInfo);
        result.setResultCode(PluginResultCode.OK);

        return result;
    }

    private void checkPluginCompatibility(PluginInfo pluginInfo) throws Exception {
        Version applicationVersion = Version.valueOf(Client.getApplicationVersion());
        Version pluginAppMinVersion = Version.valueOf(pluginInfo.getPluginAppMinVersion());

        logger.log(Level.INFO, "Checking plugin compatibility:");
        logger.log(Level.INFO, "- Application version:             " + Client.getApplicationVersion() + "("
                + applicationVersion + ")");
        logger.log(Level.INFO, "- Plugin min. application version: " + pluginInfo.getPluginAppMinVersion() + "("
                + pluginAppMinVersion + ")");

        if (applicationVersion.lessThan(pluginAppMinVersion)) {
            throw new Exception(
                    "Plugin is incompatible to this application version. Plugin min. application version is "
                            + pluginInfo.getPluginAppMinVersion() + ", current application version is "
                            + Client.getApplicationVersion());
        }

        // Verify if any conflicting plugins are installed
        logger.log(Level.INFO, "Checking for conflicting plugins.");

        List<String> conflictingIds = pluginInfo.getConflictingPluginIds();
        List<String> conflictingInstalledIds = new ArrayList<String>();

        if (conflictingIds != null) {
            for (String pluginId : conflictingIds) {
                Plugin plugin = Plugins.get(pluginId);

                if (plugin != null) {
                    logger.log(Level.INFO, "- Conflicting plugin " + pluginId + " found.");
                    conflictingInstalledIds.add(pluginId);
                }

                logger.log(Level.FINE, "- Conflicting plugin " + pluginId + " not installed");
            }
        }

        result.setConflictingPlugins(conflictingInstalledIds);
    }

    private String calculateChecksum(File tempPluginJarFile) throws Exception {
        CipherUtil.enableUnlimitedStrength();

        byte[] actualChecksum = FileUtil.createChecksum(tempPluginJarFile, "SHA256");
        return StringUtil.toHex(actualChecksum);
    }

    private PluginOperationResult executeInstallFromLocalFile(File pluginJarFile) throws Exception {
        eventBus.post(new PluginInstallExternalEvent(pluginJarFile.getAbsolutePath()));

        PluginInfo pluginInfo = readPluginInfoFromJar(pluginJarFile);

        checkPluginNotInstalled(pluginInfo.getPluginId());
        checkPluginCompatibility(pluginInfo);

        File targetPluginJarFile = installPlugin(pluginJarFile, pluginInfo);

        result.setSourcePluginPath(pluginJarFile.getPath());
        result.setTargetPluginPath(targetPluginJarFile.getPath());
        result.setAffectedPluginInfo(pluginInfo);
        result.setResultCode(PluginResultCode.OK);

        return result;
    }

    private PluginOperationResult executeInstallFromUrl(String downloadJarUrl) throws Exception {
        eventBus.post(new PluginInstallExternalEvent(downloadJarUrl));

        File tempPluginJarFile = downloadPluginJar(downloadJarUrl);
        PluginInfo pluginInfo = readPluginInfoFromJar(tempPluginJarFile);

        checkPluginNotInstalled(pluginInfo.getPluginId());
        checkPluginCompatibility(pluginInfo);

        File targetPluginJarFile = installPlugin(tempPluginJarFile, pluginInfo);

        result.setSourcePluginPath(downloadJarUrl);
        result.setTargetPluginPath(targetPluginJarFile.getPath());
        result.setAffectedPluginInfo(pluginInfo);
        result.setResultCode(PluginResultCode.OK);

        return result;
    }

    private void checkPluginNotInstalled(String pluginId) throws Exception {
        Plugin locallyInstalledPlugin = Plugins.get(pluginId);

        if (locallyInstalledPlugin != null) {
            throw new Exception("Plugin '" + pluginId + "' already installed. Use 'sy plugin remove " + pluginId
                    + "' to uninstall it first.");
        }

        logger.log(Level.INFO, "Plugin '" + pluginId + "' not installed. Okay!");
    }

    private PluginInfo readPluginInfoFromJar(File pluginJarFile) throws Exception {
        try (JarInputStream jarStream = new JarInputStream(new FileInputStream(pluginJarFile))) {
            Manifest jarManifest = jarStream.getManifest();

            if (jarManifest == null) {
                throw new Exception(
                        "Given file is not a valid Syncany plugin file (not a JAR file, or no manifest).");
            }

            String pluginId = jarManifest.getMainAttributes().getValue("Plugin-Id");

            if (pluginId == null) {
                throw new Exception("Given file is not a valid Syncany plugin file (no plugin ID in manifest).");
            }

            PluginInfo pluginInfo = new PluginInfo();

            pluginInfo.setPluginId(pluginId);
            pluginInfo.setPluginName(jarManifest.getMainAttributes().getValue("Plugin-Name"));
            pluginInfo.setPluginVersion(jarManifest.getMainAttributes().getValue("Plugin-Version"));
            pluginInfo.setPluginDate(jarManifest.getMainAttributes().getValue("Plugin-Date"));
            pluginInfo.setPluginAppMinVersion(jarManifest.getMainAttributes().getValue("Plugin-App-Min-Version"));
            pluginInfo.setPluginRelease(
                    Boolean.parseBoolean(jarManifest.getMainAttributes().getValue("Plugin-Release")));

            if (jarManifest.getMainAttributes().getValue("Plugin-Conflicts-With") != null) {
                pluginInfo.setConflictingPluginIds(
                        Arrays.asList(jarManifest.getMainAttributes().getValue("Plugin-Conflicts-With")));
            }

            return pluginInfo;
        }
    }

    private File installPlugin(File pluginJarFile, PluginInfo pluginInfo) throws IOException {
        File globalUserPluginDir = UserConfig.getUserPluginLibDir();
        globalUserPluginDir.mkdirs();

        File targetPluginJarFile = new File(globalUserPluginDir,
                String.format("syncany-plugin-%s-%s.jar", pluginInfo.getPluginId(), pluginInfo.getPluginVersion()));

        logger.log(Level.INFO, "Installing plugin from " + pluginJarFile + " to " + targetPluginJarFile + " ...");
        FileUtils.copyFile(pluginJarFile, targetPluginJarFile);

        return targetPluginJarFile;
    }

    /**
     * Downloads the plugin JAR from the given URL to a temporary
     * local location.
     */
    private File downloadPluginJar(String pluginJarUrl) throws Exception {
        URL pluginJarFile = new URL(pluginJarUrl);
        logger.log(Level.INFO, "Querying " + pluginJarFile + " ...");

        URLConnection urlConnection = pluginJarFile.openConnection();
        urlConnection.setConnectTimeout(2000);
        urlConnection.setReadTimeout(2000);

        File tempPluginFile = File.createTempFile("syncany-plugin", "tmp");
        tempPluginFile.deleteOnExit();

        logger.log(Level.INFO, "Downloading to " + tempPluginFile + " ...");
        FileOutputStream tempPluginFileOutputStream = new FileOutputStream(tempPluginFile);
        InputStream remoteJarFileInputStream = urlConnection.getInputStream();

        IOUtils.copy(remoteJarFileInputStream, tempPluginFileOutputStream);

        remoteJarFileInputStream.close();
        tempPluginFileOutputStream.close();

        if (!tempPluginFile.exists() || tempPluginFile.length() == 0) {
            throw new Exception("Downloading plugin file failed, URL was " + pluginJarUrl);
        }

        return tempPluginFile;
    }

    private PluginOperationResult executeList() throws Exception {
        final Version applicationVersion = Version.valueOf(Client.getApplicationVersion());
        Map<String, ExtendedPluginInfo> pluginInfos = new TreeMap<String, ExtendedPluginInfo>();

        // First, list local plugins
        if (options.getListMode() == PluginListMode.ALL || options.getListMode() == PluginListMode.LOCAL) {
            for (PluginInfo localPluginInfo : getLocalList()) {
                if (options.getPluginId() != null && !localPluginInfo.getPluginId().equals(options.getPluginId())) {
                    continue;
                }

                // Determine standard plugin information
                ExtendedPluginInfo extendedPluginInfo = new ExtendedPluginInfo();

                extendedPluginInfo.setLocalPluginInfo(localPluginInfo);
                extendedPluginInfo.setInstalled(true);

                // Test if plugin can be uninstalled
                Plugin plugin = Plugins.get(localPluginInfo.getPluginId());
                File pluginJarFile = getJarFile(plugin);
                boolean canUninstall = canUninstall(pluginJarFile);

                extendedPluginInfo.setCanUninstall(canUninstall);

                // Add to list
                pluginInfos.put(localPluginInfo.getPluginId(), extendedPluginInfo);
            }
        }

        // Then, list remote plugins
        if (options.getListMode() == PluginListMode.ALL || options.getListMode() == PluginListMode.REMOTE) {
            for (PluginInfo remotePluginInfo : getRemotePluginInfoList()) {
                if (options.getPluginId() != null
                        && !remotePluginInfo.getPluginId().equals(options.getPluginId())) {
                    continue;
                }

                ExtendedPluginInfo extendedPluginInfo = pluginInfos.get(remotePluginInfo.getPluginId());
                boolean localPluginInstalled = extendedPluginInfo != null;

                if (!localPluginInstalled) { // Locally not installed
                    extendedPluginInfo = new ExtendedPluginInfo();

                    extendedPluginInfo.setInstalled(false);
                    extendedPluginInfo.setRemoteAvailable(true);
                } else { // Locally also installed
                    extendedPluginInfo.setRemoteAvailable(true);

                    Version localVersion = Version
                            .valueOf(extendedPluginInfo.getLocalPluginInfo().getPluginVersion());
                    Version remoteVersion = Version.valueOf(remotePluginInfo.getPluginVersion());
                    Version remoteMinAppVersion = Version.valueOf(remotePluginInfo.getPluginAppMinVersion());

                    boolean localVersionOutdated = localVersion.lessThan(remoteVersion);
                    boolean applicationVersionCompatible = applicationVersion
                            .greaterThanOrEqualTo(remoteMinAppVersion);
                    boolean pluginIsOutdated = localVersionOutdated && applicationVersionCompatible;

                    extendedPluginInfo.setOutdated(pluginIsOutdated);
                }

                extendedPluginInfo.setRemotePluginInfo(remotePluginInfo);
                pluginInfos.put(remotePluginInfo.getPluginId(), extendedPluginInfo);
            }
        }

        result.setPluginList(new ArrayList<ExtendedPluginInfo>(pluginInfos.values()));
        result.setResultCode(PluginResultCode.OK);

        return result;
    }

    private List<PluginInfo> getLocalList() {
        List<PluginInfo> localPluginInfos = new ArrayList<PluginInfo>();

        for (Plugin plugin : Plugins.list()) {
            PluginInfo pluginInfo = new PluginInfo();

            pluginInfo.setPluginId(plugin.getId());
            pluginInfo.setPluginName(plugin.getName());
            pluginInfo.setPluginVersion(plugin.getVersion());

            localPluginInfos.add(pluginInfo);
        }

        return localPluginInfos;
    }

    private List<PluginInfo> getRemotePluginInfoList() throws Exception {
        String remoteListStr = getRemoteListStr(null);
        PluginListResponse pluginListResponse = new Persister().read(PluginListResponse.class, remoteListStr);

        return pluginListResponse.getPlugins();
    }

    private PluginInfo getRemotePluginInfo(String pluginId) throws Exception {
        String remoteListStr = getRemoteListStr(pluginId);
        PluginListResponse pluginListResponse = new Persister().read(PluginListResponse.class, remoteListStr);

        if (pluginListResponse.getPlugins().size() > 0) {
            return pluginListResponse.getPlugins().get(0);
        } else {
            return null;
        }
    }

    private String getRemoteListStr(String pluginId) throws Exception {
        String appVersion = Client.getApplicationVersion();
        String snapshotsEnabled = (options.isSnapshots()) ? "true" : "false";
        String pluginIdQueryStr = (pluginId != null) ? pluginId : "";
        String osStr = EnvironmentUtil.getOperatingSystemDescription();
        String archStr = EnvironmentUtil.getArchDescription();

        String apiEndpointUrl = (options.getApiEndpoint() != null) ? options.getApiEndpoint()
                : API_DEFAULT_ENDPOINT_URL;
        URL pluginListUrl = new URL(String.format(API_PLUGIN_LIST_REQUEST_FORMAT, apiEndpointUrl, appVersion,
                snapshotsEnabled, pluginIdQueryStr, osStr, archStr));

        logger.log(Level.INFO, "Querying " + pluginListUrl + " ...");
        eventBus.post(new ConnectToHostExternalEvent(pluginListUrl.getHost()));

        URLConnection urlConnection = pluginListUrl.openConnection();
        urlConnection.setConnectTimeout(2000);
        urlConnection.setReadTimeout(2000);

        BufferedReader urlStreamReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
        StringBuilder responseStringBuilder = new StringBuilder();

        String line;
        while ((line = urlStreamReader.readLine()) != null) {
            responseStringBuilder.append(line);
        }

        String responseStr = responseStringBuilder.toString();
        logger.log(Level.INFO, "Response from api.syncany.org: " + responseStr);

        return responseStr;
    }
}