org.cytoscape.app.internal.net.WebQuerier.java Source code

Java tutorial

Introduction

Here is the source code for org.cytoscape.app.internal.net.WebQuerier.java

Source

package org.cytoscape.app.internal.net;

/*
 * #%L
 * Cytoscape App Impl (app-impl)
 * $Id:$
 * $HeadURL:$
 * %%
 * Copyright (C) 2008 - 2013 The Cytoscape Consortium
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 2.1 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 Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
 * #L%
 */

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.cytoscape.app.internal.exception.AppDownloadException;
import org.cytoscape.app.internal.manager.App;
import org.cytoscape.app.internal.manager.AppManager;
import org.cytoscape.app.internal.manager.AppParser;
import org.cytoscape.app.internal.manager.AppParser.ChecksumException;
import org.cytoscape.app.internal.net.WebApp.Release;
import org.cytoscape.app.internal.ui.downloadsites.DownloadSite;
import org.cytoscape.app.internal.util.DebugHelper;
import org.cytoscape.application.CyVersion;
import org.cytoscape.io.util.StreamUtil;
import org.cytoscape.work.TaskMonitor;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.SwingUtilities;
import javax.swing.JOptionPane;

/**
 * This class is responsible for querying the Cytoscape App Store web service to obtain
 * information about available apps and app updates.
 */
public class WebQuerier {

    public static final List<DownloadSite> DEFAULT_DOWNLOAD_SITES = new LinkedList<DownloadSite>();

    public static final String DEFAULT_APP_STORE_URL = "http://apps.cytoscape.org/";

    private static final String REQUEST_JSON_HEADER_KEY = "X-Requested-With";
    private static final String REQUEST_JSON_HEADER_VALUE = "XMLHttpRequest";

    static {
        DownloadSite site = new DownloadSite();
        site.setSiteName("Cytoscape App Store");
        site.setSiteUrl(DEFAULT_APP_STORE_URL);
        DEFAULT_DOWNLOAD_SITES.add(site);
    }

    /**
     * A regular expression for version lists that are compatible with the current version of Cytoscape.
     */
    private static final String COMPATIBLE_RELEASE_REGEX = "(^\\s*|.*,)\\s*3(\\..*)?\\s*(\\s*$|,.*)";

    private static final Logger logger = LoggerFactory.getLogger(WebQuerier.class);

    private StreamUtil streamUtil;

    private CyVersion cyVersion;

    /** A reference to the result obtained by the last successful query for all available apps. */
    // private Set<WebApp> apps;

    /** A reference to a map which keeps track of the known set of apps for each known tag */
    // private Map<String, Set<WebApp>> appsByTagName;

    /** A reference to the result obtained by the last successful query for all available app tags. */
    // private Map<String, AppTag> appTags;

    /**
     * A reference to the result obtained by the last successful query for all available apps
     * to this app store URL.
     */
    private Map<String, Set<WebApp>> appsByUrl;

    /** 
     * A reference to a map of maps keeping track, for each app store URL, the known set of 
     * apps for each known tag from that URL.
     */
    private Map<String, Map<String, Set<WebApp>>> appsByTagNameByUrl;

    /**
     * A reference to the set of all known tags for a given app store URL. The tags are stored
     * in a map that maps the tag's string name to a tag object containing more information
     * about the tag, such as the number of apps associated with it.
     */
    private Map<String, Map<String, AppTag>> appTagsByUrl;

    private String currentAppStoreUrl = DEFAULT_APP_STORE_URL;

    private static final Pattern VERSION_PATTERN = Pattern
            .compile("(\\d+)([.](\\d+)([.](\\d+)([.]([-_a-zA-Z0-9]+))?)?)?");

    public static final Pattern OUTPUT_FILENAME_DISALLOWED_CHARACTERS = Pattern.compile("[^a-zA-Z0-9.-]");

    private AppManager appManager = null;

    /**
     * A class that represents a tag used for apps, containing information about the tag
     * such as its unique name used on the app store website as well as its human-readable name.
     */
    public class AppTag {

        /** A unique name of the tag used by the app store website as a tag identifier */
        private String name;

        /** The name of the tag that is shown to the user */
        private String fullName;

        /** The number of apps associated with this tag */
        private int count;

        public AppTag() {
        }

        /** Obtain the name of the tag, which is a unique name used by the app store website as an identifier */
        public String getName() {
            return name;
        }

        /** Obtain the name of the tag that is shown to the user */
        public String getFullName() {
            return fullName;
        }

        /** Obtain the number of apps known by the web store to be associated with this tag */
        public int getCount() {
            return count;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void setFullName(String fullName) {
            this.fullName = fullName;
        }

        public void setCount(int count) {
            this.count = count;
        }

        @Override
        public String toString() {
            return fullName + " (" + count + ")";
        }
    }

    public WebQuerier(StreamUtil streamUtil, CyVersion cyVersion) {
        this.streamUtil = streamUtil;
        this.cyVersion = cyVersion;

        /*
        // *** Older initialization for previous implementation supporting a single app store page
        apps = null;
        appTags = new HashMap<String, AppTag>();
        appsByTagName = new HashMap<String, Set<WebApp>>();
        */

        appsByUrl = new HashMap<String, Set<WebApp>>();
        appTagsByUrl = new HashMap<String, Map<String, AppTag>>();
        appsByTagNameByUrl = new HashMap<String, Map<String, Set<WebApp>>>();

        appsByUrl.put(currentAppStoreUrl, null);
        appTagsByUrl.put(currentAppStoreUrl, new HashMap<String, AppTag>());
        appsByTagNameByUrl.put(currentAppStoreUrl, new HashMap<String, Set<WebApp>>());

        /*
        Set<WebApp> webApps = getAllApps();
            
        DebugHelper.print("Apps found: " + webApps.size());
        */
    }

    /**
     * Makes a HTTP query using the given URL and returns the response as a string.
     * @param url The URL used to make the HTTP request
     * @return The response, as a string
     * @throws IOException If there was an error while attempting to make a connection
     * to the given URL
     */
    private String query(String url) throws IOException {
        // Convert the string url to a URL object
        URL parsedUrl = null;
        try {
            parsedUrl = new URL(url);
        } catch (MalformedURLException e) {
            throw new IOException("Malformed url, " + e.getMessage());
        }

        String result = null;

        HttpURLConnection connection = (HttpURLConnection) streamUtil.getURLConnection(parsedUrl);
        connection.setRequestProperty(REQUEST_JSON_HEADER_KEY, REQUEST_JSON_HEADER_VALUE);
        connection.connect();

        InputStream inputStream = connection.getInputStream();
        result = IOUtils.toString(inputStream, "UTF-8");

        connection.disconnect();

        return result;
    }

    public void setAppManager(AppManager appManager) {
        this.appManager = appManager;
    }

    public String getDefaultAppStoreUrl() {
        return DEFAULT_APP_STORE_URL;
    }

    public String getCurrentAppStoreUrl() {
        return currentAppStoreUrl;
    }

    /**
     * Sets the current base url used for app store queries. If the url is malformed,
     * no change is made.
     * 
     * @param url The base url of the app store, e.g. http://apps.cytoscape.org/
     */
    public void setCurrentAppStoreUrl(String url) {
        boolean malformed = false;

        try {
            URL checkMalformed = new URL(url);
        } catch (MalformedURLException e) {
            malformed = true;
            logger.warn("Malformed URL: " + url + ", " + e.getMessage());
        }

        if (!malformed) {
            if (!url.trim().endsWith("/")) {
                currentAppStoreUrl = url + "/";
            } else {
                currentAppStoreUrl = url;
            }

            if (appsByUrl.get(currentAppStoreUrl) == null) {
                appsByUrl.put(currentAppStoreUrl, null);
            }

            if (appTagsByUrl.get(currentAppStoreUrl) == null) {
                appTagsByUrl.put(currentAppStoreUrl, new HashMap<String, AppTag>());
            }

            if (appsByTagNameByUrl.get(currentAppStoreUrl) == null) {
                appsByTagNameByUrl.put(currentAppStoreUrl, new HashMap<String, Set<WebApp>>());
            }
        }
    }

    /**
     * Return the set of all tag names found on the app store. 
     * @return The set of all available tag names
     */
    public Set<AppTag> getAllTags() {
        // Make a query for all apps if not done so; tag information for each app is returned
        // by the web store and is used to build a set of all available tags
        Set<WebApp> apps = getAllApps();

        return new HashSet<AppTag>(appTagsByUrl.get(currentAppStoreUrl).values());
    }

    public boolean appsHaveBeenLoaded() {
        return this.appsByUrl.get(currentAppStoreUrl) != null;
    }

    public Set<WebApp> getAllApps() {
        // If we have a cached result from the previous query, use that one
        if (this.appsByUrl.get(currentAppStoreUrl) != null) {
            return this.appsByUrl.get(currentAppStoreUrl);
        }

        DebugHelper.print("Obtaining apps from app store..");

        Set<WebApp> result = new HashSet<WebApp>();

        String jsonResult = null;
        try {
            // Obtain information about the app from the website
            jsonResult = query(currentAppStoreUrl + "backend/all_apps");

            if (appManager != null && appManager.getAppManagerDialog() != null) {
                appManager.getAppManagerDialog().hideNetworkError();
            }

            // Parse the JSON result
            JSONArray jsonArray = new JSONArray(jsonResult);
            JSONObject jsonObject = null;
            String keyName;

            for (int index = 0; index < jsonArray.length(); index++) {
                jsonObject = jsonArray.getJSONObject(index);

                WebApp webApp = new WebApp();

                keyName = "fullname";
                if (jsonObject.has(keyName)) {
                    webApp.setName(jsonObject.get(keyName).toString());
                    webApp.setFullName(jsonObject.get(keyName).toString());
                } else {
                    continue;
                }

                keyName = "icon_url";
                if (jsonObject.has(keyName)) {
                    webApp.setIconUrl(jsonObject.get(keyName).toString());
                }

                keyName = "page_url";
                if (jsonObject.has(keyName)) {
                    webApp.setPageUrl(currentAppStoreUrl.substring(0, currentAppStoreUrl.length() - 1)
                            + jsonObject.get(keyName).toString());
                }

                keyName = "description";
                if (jsonObject.has(keyName)) {
                    webApp.setDescription(jsonObject.get(keyName).toString());
                }

                keyName = "downloads";
                if (jsonObject.has(keyName)) {
                    try {
                        webApp.setDownloadCount(Integer.parseInt(jsonObject.get(keyName).toString()));
                    } catch (NumberFormatException e) {
                    }
                }

                keyName = "stars_percentage";
                if (jsonObject.has(keyName)) {
                    try {
                        webApp.setStarsPercentage(Integer.parseInt(jsonObject.get(keyName).toString()));
                    } catch (NumberFormatException e) {
                    }
                }

                keyName = "votes";
                if (jsonObject.has(keyName)) {
                    try {
                        webApp.setVotes(Integer.parseInt(jsonObject.get(keyName).toString()));
                    } catch (NumberFormatException e) {
                    }
                }

                keyName = "citation";
                if (jsonObject.has(keyName)) {
                    webApp.setCitation(jsonObject.get(keyName).toString());
                }

                try {
                    List<WebApp.Release> releases = new LinkedList<WebApp.Release>();

                    if (jsonObject.has("releases")) {
                        JSONArray jsonReleases = jsonObject.getJSONArray("releases");
                        JSONObject jsonRelease;
                        boolean isCompatible = true;

                        for (int releaseIndex = 0; releaseIndex < jsonReleases.length(); releaseIndex++) {
                            jsonRelease = jsonReleases.getJSONObject(releaseIndex);

                            WebApp.Release release = new WebApp.Release();

                            release.setBaseUrl(currentAppStoreUrl);
                            release.setRelativeUrl(jsonRelease.optString("release_download_url"));
                            release.setReleaseDate(jsonRelease.optString("created_iso"));
                            release.setReleaseVersion(jsonRelease.optString("version"));
                            release.setSha512Checksum(jsonRelease.optString("hexchecksum"));

                            keyName = "works_with";
                            if (jsonRelease.has(keyName)) {
                                release.setCompatibleCytoscapeVersions(jsonRelease.get(keyName).toString());
                                isCompatible = release.isCompatible(cyVersion);
                            }

                            if (isCompatible)
                                releases.add(release);
                        }

                        // Sort releases by version number
                        Collections.sort(releases, new Comparator<WebApp.Release>() {

                            @Override
                            public int compare(Release first, Release second) {
                                return compareVersions(second.getReleaseVersion(), first.getReleaseVersion());
                            }

                        });
                    }

                    webApp.setReleases(releases);
                } catch (JSONException e) {
                    logger.warn(
                            "Error obtaining releases for app: " + webApp.getFullName() + ", " + e.getMessage());
                }

                // DebugHelper.print("Obtaining ImageIcon: " + iconUrlPrefix + webApp.getIconUrl());
                // webApp.setImageIcon(new ImageIcon(new URL(iconUrlPrefix + webApp.getIconUrl())));

                // Check the app for compatible releases
                List<WebApp.Release> compatibleReleases = getCompatibleReleases(webApp);

                // Only add this app if it has compatible releases
                if (compatibleReleases.size() > 0) {
                    // Obtain tags associated with this app
                    processAppTags(webApp, jsonObject);

                    result.add(webApp);
                }
            }

        } catch (final IOException e) {
            if (appManager != null && appManager.getAppManagerDialog() != null) {
                appManager.getAppManagerDialog().showNetworkError();
            }
            e.printStackTrace();
            result = null;
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            DebugHelper.print("Error parsing JSON: " + e.getMessage());
            e.printStackTrace();
        }

        //DebugHelper.print(result.size() + " apps found from web store.");

        // Cache the result of this query
        this.appsByUrl.put(currentAppStoreUrl, result);
        return result;
    }

    private void processAppTags(WebApp webApp, JSONObject jsonObject) throws JSONException {
        // Obtain tags associated with this app from the JSONObject representing the app data in JSON format obtained
        // from the web store

        JSONArray appTagObjects = jsonObject.optJSONArray("tags");
        if (appTagObjects == null) {
            return;
        }

        for (int index = 0; index < appTagObjects.length(); index++) {
            /*
            JSONObject appTagObject = appTagObjects.getJSONObject(index);
                
            String appTagName = appTagObject.get("fullname").toString();
            */

            String appTagName = appTagObjects.get(index).toString();

            AppTag appTag = appTagsByUrl.get(currentAppStoreUrl).get(appTagName);

            if (appTag == null) {
                appTag = new AppTag();
                appTag.setName(appTagName);
                // appTag.setFullName(appTagObject.get("fullname").toString());
                appTag.setFullName(appTagName);

                appTag.setCount(0);
                appTagsByUrl.get(currentAppStoreUrl).put(appTagName, appTag);
            }

            webApp.getAppTags().add(appTag);

            // Add the app information for this tag to the map which keeps apps categorized by tag
            if (appsByTagNameByUrl.get(currentAppStoreUrl).get(appTagName) == null) {
                appsByTagNameByUrl.get(currentAppStoreUrl).put(appTagName, new HashSet<WebApp>());
            }

            appsByTagNameByUrl.get(currentAppStoreUrl).get(appTagName).add(webApp);
            appTag.setCount(appTag.getCount() + 1);
        }
    }

    /**
     * Given the unique app name used by the app store, query the app store for the 
     * download URL and download the app to the given directory.
     * 
     * If a file with the same name exists in the directory, it is overwritten.
     * 
     * @param appName The unique app name used by the app store
     * @param version The desired version, or <code>null</code> to obtain the latest release
     * @param directory The directory used to store the downloaded file
     * @param taskMonitor 
     */
    public File downloadApp(WebApp webApp, String version, File directory, DownloadStatus status)
            throws AppDownloadException {

        List<WebApp.Release> compatibleReleases = getCompatibleReleases(webApp);

        if (compatibleReleases.size() > 0) {
            WebApp.Release releaseToDownload = null;

            if (version != null) {
                for (WebApp.Release compatibleRelease : compatibleReleases) {

                    // Check if the desired version is found in the list of available versions
                    if (compatibleRelease.getReleaseVersion()
                            .matches("(^\\s*|.*,)\\s*" + version + "\\s*(\\s*$|,.*)")) {
                        releaseToDownload = compatibleRelease;
                    }
                }

                if (releaseToDownload == null) {
                    throw new AppDownloadException("No release with the requested version " + version
                            + " was found for the requested app " + webApp.getFullName());
                }
            } else {
                releaseToDownload = compatibleReleases.get(compatibleReleases.size() - 1);
            }

            URL downloadUrl = null;
            try {
                downloadUrl = new URL(currentAppStoreUrl + releaseToDownload.getRelativeUrl());
            } catch (MalformedURLException e) {
                throw new AppDownloadException("Unable to obtain URL for version " + version
                        + " of the release for " + webApp.getFullName());
            }

            if (downloadUrl != null) {
                try {

                    // Prepare to download
                    URLConnection connection = streamUtil.getURLConnection(downloadUrl);
                    InputStream inputStream = connection.getInputStream();
                    long contentLength = connection.getContentLength();
                    ReadableByteChannel readableByteChannel = Channels.newChannel(inputStream);

                    File outputFile;
                    try {
                        // Replace spaces with underscores
                        String outputFileBasename = webApp.getName().replaceAll("\\s", "_");

                        // Append version information
                        outputFileBasename += "-v" + releaseToDownload.getReleaseVersion();

                        // Strip disallowed characters
                        outputFileBasename = OUTPUT_FILENAME_DISALLOWED_CHARACTERS.matcher(outputFileBasename)
                                .replaceAll("");

                        // Append extension
                        outputFileBasename += ".jar";

                        // Output file has same name as app, but spaces and slashes are replaced with hyphens
                        outputFile = new File(directory.getCanonicalPath() + File.separator + outputFileBasename);

                        if (outputFile.exists()) {
                            outputFile.delete();
                        }

                        outputFile.createNewFile();

                        FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
                        try {
                            FileChannel fileChannel = fileOutputStream.getChannel();

                            long currentDownloadPosition = 0;
                            long bytesTransferred;

                            TaskMonitor taskMonitor = status.getTaskMonitor();
                            do {
                                bytesTransferred = fileChannel.transferFrom(readableByteChannel,
                                        currentDownloadPosition, 1 << 14);
                                if (status.isCanceled()) {
                                    outputFile.delete();
                                    return null;
                                }
                                currentDownloadPosition += bytesTransferred;
                                if (contentLength > 0) {
                                    double progress = (double) currentDownloadPosition / contentLength;
                                    taskMonitor.setProgress(progress);
                                }
                            } while (bytesTransferred > 0);
                        } finally {
                            fileOutputStream.close();
                        }
                    } finally {
                        readableByteChannel.close();
                    }
                    return outputFile;
                } catch (IOException e) {
                    throw new AppDownloadException(
                            "Error while downloading app " + webApp.getFullName() + ", " + e.getMessage());
                }
            }
        } else {
            throw new AppDownloadException(
                    "No available releases were found for the app " + webApp.getFullName() + ".");
        }
        return null;
    }

    /**
     * Returns the list of compatible releases, in chronological order starting with the earliest.
     * @param webApp
     * @return
     */
    private List<WebApp.Release> getCompatibleReleases(WebApp webApp) {
        List<WebApp.Release> compatibleReleases = new LinkedList<WebApp.Release>();

        for (WebApp.Release release : webApp.getReleases()) {

            // Get releases that are compatible with the current version of Cytoscape (version 3)
            if (release.getCompatibleCytoscapeVersions().matches(COMPATIBLE_RELEASE_REGEX)) {
                compatibleReleases.add(release);
            }
        }

        return compatibleReleases;
    }

    public Set<WebApp> getAppsByTag(String tagName) {
        // Query for apps (which includes tag information) if not done so
        Set<WebApp> webApps = getAllApps();

        return appsByTagNameByUrl.get(currentAppStoreUrl).get(tagName);
    }

    public Set<Update> checkForUpdates(Set<App> apps, AppManager appManager) {
        Set<Update> updates = new HashSet<Update>();

        Update update;
        for (App app : apps) {
            for (String url : appsByUrl.keySet()) {
                update = checkForUpdate(app, url, appManager);

                if (update != null) {
                    updates.add(update);
                    break;
                }
            }
        }

        return updates;
    }

    public void checkWebAppInstallStatus(Set<WebApp> webApps, AppManager appManager) {

        // This method contains a nest structure with 3 for loops. However,
        // there isn't much other way to check every hash of every WebApp with every
        // installed app. Runtime performance is
        // (num_installed_apps * num_web_apps * releases_per_web_app)

        for (App app : appManager.getApps()) {

            if (app.getSha512Checksum() == null) {
                try {
                    app.setSha512Checksum(appManager.getAppParser().getChecksum(app.getAppFile()));
                } catch (ChecksumException e) {
                    app.setSha512Checksum(null);
                }
            }

            if (app.getSha512Checksum() != null) {
                String sha512checksum = app.getSha512Checksum().toLowerCase();

                for (WebApp webApp : webApps) {

                    List<Release> releases = webApp.getReleases();

                    for (Release release : releases) {

                        if (release.getSha512Checksum().trim().length() > 0
                                && sha512checksum.indexOf(release.getSha512Checksum()) != -1) {

                            webApp.setCorrespondingApp(app);

                            // For convenience, set the app's description field
                            if (app.getDescription() == null) {
                                app.setDescription(webApp.getDescription());
                            }
                        }
                    }
                }
            }
        }
    }

    private Update checkForUpdate(App app, String url, AppManager appManager) {
        Set<WebApp> urlApps = appsByUrl.get(url);

        if (urlApps != null) {

            // Look for an app with same name
            for (WebApp webApp : urlApps) {

                if (webApp.getName().equalsIgnoreCase(app.getAppName())) {

                    WebApp.Release highestVersionRelease = null;
                    for (WebApp.Release release : webApp.getReleases()) {

                        if (highestVersionRelease == null
                                || compareVersions(highestVersionRelease.getReleaseVersion(),
                                        release.getReleaseVersion()) > 0) {

                            highestVersionRelease = release;
                        }
                    }

                    if (highestVersionRelease != null
                            && compareVersions(highestVersionRelease.getReleaseVersion(), app.getVersion()) < 0) {

                        Update update = new Update();
                        update.setUpdateVersion(highestVersionRelease.getReleaseVersion());
                        update.setApp(app);
                        update.setWebApp(webApp);
                        update.setRelease(highestVersionRelease);

                        return update;
                    }
                }
            }
        }

        return null;
    }

    // Find which version is more recent, assuming versions are in format x.y[.z[.qualifier]]

    /**
     * Compares 2 versions, assuming they are in format x.y[.z[.qualifier]], returning a negative
     * number if the first is more recent, a positive number if the second is more recent,
     * or 0 if the versions were the same or unable to determine which is more recent.
     * 
     * @param version1 The first version
     * @param version2 The second version
     * @return A negative integer if first more recent, a positive integer if second more recent,
     * or 0 if the versions were the same or unable to determine which is more recent.
     */
    public static int compareVersions(String version1, String version2) {
        Matcher matcher1 = VERSION_PATTERN.matcher(version1);
        Matcher matcher2 = VERSION_PATTERN.matcher(version2);

        if (!matcher1.matches()) {
            throw new IllegalArgumentException("Incorrectly-formatted version string: " + version1);
        }
        if (!matcher2.matches()) {
            throw new IllegalArgumentException("Incorrectly-formatted version string: " + version2);
        }

        // major = 1, minor = 3, micro = 5, qualifier = 7
        for (int i = 1; i < 8; i += 2) {
            String part1 = matcher1.group(i);
            String part2 = matcher2.group(i);

            if (part1 == null && part2 == null) {
                return 0;
            }
            if (i < 7) {
                // major/minor/micro
                if (part1 != null && part2 == null) {
                    return Integer.parseInt(part1) == 0 ? 0 : -1;
                }
                if (part1 == null && part2 != null) {
                    return Integer.parseInt(part2) == 0 ? 0 : 1;
                }
            } else {
                // qualifier
                if (part1 != null && part2 == null) {
                    return -1;
                }
                if (part1 == null && part2 != null) {
                    return 1;
                }
            }
            int result = part1.compareTo(part2);
            if (result != 0) {
                return -result;
            }
        }
        return 0;
    }

    public void findAppDescriptions(Set<App> apps) {
        // TODO: Perhaps modify this method to do the check with a given app store
        // as a parameter, instead of all app stores

        if (appsByUrl.get(DEFAULT_APP_STORE_URL) == null) {
            return;
        }

        // Find the set of all available apps
        Set<WebApp> allWebApps = new HashSet<WebApp>(appsByUrl.get(DEFAULT_APP_STORE_URL).size());

        for (String url : appsByUrl.keySet()) {

            Set<WebApp> urlApps = appsByUrl.get(url);
            for (WebApp webApp : urlApps) {

                allWebApps.add(webApp);
            }
        }

        // Find set of all app releases
        Map<Release, WebApp> allReleases = new HashMap<Release, WebApp>(
                appsByUrl.get(DEFAULT_APP_STORE_URL).size());

        List<Release> appReleases = null;
        for (WebApp webApp : allWebApps) {

            appReleases = webApp.getReleases();

            for (Release appRelease : appReleases) {
                allReleases.put(appRelease, webApp);
            }
        }

        // Find matching app hashes
        for (Release release : allReleases.keySet()) {

            for (App app : apps) {
                String checksum = app.getSha512Checksum().toLowerCase();

                // TODO: Currently, this will give the app the description from the
                // first app store providing the matching hash. Perhaps
                // we could give the default app store the priority in providing the description,
                // in cases where multiple stores give the same hash.
                if (checksum.indexOf(release.getSha512Checksum().toLowerCase()) != -1
                        && app.getDescription() == null) {

                    // WebQuerier obtains app information from app store because no description metadata is required
                    // in the app zip file itself. This was to allow better App-Bundle interchangeability, not
                    // imposing unneeded restrictions on OSGi bundles (from past discussion on mailing list, some time in 2012)
                    //               System.out.println("Found description: " + allReleases.get(release).getDescription());
                    app.setDescription(allReleases.get(release).getDescription());
                }
            }
        }
    }
}