org.cytoscape.app.internal.manager.AppManager.java Source code

Java tutorial

Introduction

Here is the source code for org.cytoscape.app.internal.manager.AppManager.java

Source

package org.cytoscape.app.internal.manager;

/*
 * #%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.FileFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.cytoscape.app.AbstractCyApp;
import org.cytoscape.app.internal.event.AppStatusChangedListener;
import org.cytoscape.app.internal.event.AppsChangedEvent;
import org.cytoscape.app.internal.event.AppsChangedListener;
import org.cytoscape.app.internal.exception.AppDisableException;
import org.cytoscape.app.internal.exception.AppInstallException;
import org.cytoscape.app.internal.exception.AppParsingException;
import org.cytoscape.app.internal.exception.AppUninstallException;
import org.cytoscape.app.internal.manager.App.AppStatus;
import org.cytoscape.app.internal.net.WebQuerier;
import org.cytoscape.app.internal.util.DebugHelper;
import org.cytoscape.app.swing.CySwingAppAdapter;
import org.cytoscape.application.CyApplicationConfiguration;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.Version;
import org.osgi.service.startlevel.StartLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cytoscape.app.internal.ui.AppManagerDialog;

/**
 * This class represents an App Manager, which is capable of maintaining a list of all currently installed and available apps. The class
 * also provides functionalities for installing and uninstalling apps.
 */
public class AppManager implements FrameworkListener, AppStatusChangedListener {

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

    /** Only files with these extensions are checked when looking for apps in a given subdirectory.
     */
    private static final String[] APP_EXTENSIONS = { "jar", "kar" };

    /** Installed apps are moved to this subdirectory under the local app storage directory. */
    private static final String INSTALLED_APPS_DIRECTORY_NAME = "installed";

    /** Uninstalled apps are moved to this subdirectory under the local app storage directory. */
    private static final String UNINSTALLED_APPS_DIRECTORY_NAME = "uninstalled";

    /** Disabled apps are moved to this subdirectory under the local app storage directory. */
    private static final String DISABLED_APPS_DIRECTORY_NAME = "disabled";

    /** Apps are downloaded from the web store to this subdirectory under local app storage directory. */
    private static final String DOWNLOADED_APPS_DIRECTORY_NAME = "download-temp";

    /** Apps that are loaded are stored in this temporary directory. */
    private static final String TEMPORARY_LOADED_APPS_DIRECTORY_NAME = ".temp-installed";

    /** Apps that are to be installed on restart are stored in this directory. */
    private static final String INSTALL_RESTART_DIRECTORY_NAME = "install-on-restart";

    /** This subdirectory in the local Cytoscape storage directory is used to store app data, as 
     * well as installed and uninstalled apps. */
    private static final String APPS_DIRECTORY_NAME = "3" + File.separator + "apps";

    private static final int APP_START_LEVEL = 200;

    /** The set of all apps, represented by {@link App} objects, registered to this App Manager. */
    private Set<App> apps;

    private Set<AppsChangedListener> appListeners;

    /** An {@link AppParser} object used to parse File objects and possibly URLs into {@link App} objects
     * into a format we can more easily work with
     */
    private AppParser appParser;

    /**
     * A reference to the {@link WebQuerier} object used to make queries to the app store website.
     */
    private WebQuerier webQuerier;

    /**
     * The {@link FeaturesService} used to communicate with Apache Karaf to manage OSGi bundle based apps
     */
    //   private FeaturesService featuresService;

    // private KarService karService;

    /**
     * {@link CyApplicationConfiguration} service used to obtain the directories used to store the apps.
     */
    private CyApplicationConfiguration applicationConfiguration;

    /**
     * The {@link CySwingAppAdapter} service reference provided to the constructor of the app's {@link AbstractCyApp}-implementing class.
     */
    private CySwingAppAdapter swingAppAdapter;

    private FileAlterationMonitor fileAlterationMonitor;

    private StartLevel startLevel;

    private boolean isInitialized;

    private StartupMonitor startupMonitor;

    private AppManagerDialog appManagerDialog = null;

    /**
     * A {@link FileFilter} that accepts only files in the first depth level of a given directory
     */
    private class SingleLevelFileFilter implements FileFilter {

        private File parentDirectory;

        public SingleLevelFileFilter(File parentDirectory) {
            this.parentDirectory = parentDirectory;
        }

        @Override
        public boolean accept(File pathName) {
            if (!pathName.getParentFile().equals(parentDirectory)) {
                return false;
            } else if (pathName.isDirectory()) {
                return false;
            }

            return true;
        }
    }

    public AppManager(CySwingAppAdapter swingAppAdapter, CyApplicationConfiguration applicationConfiguration,
            final WebQuerier webQuerier, StartLevel startLevel, StartupMonitor startupMonitor) {
        this.applicationConfiguration = applicationConfiguration;
        this.swingAppAdapter = swingAppAdapter;
        this.webQuerier = webQuerier;
        webQuerier.setAppManager(this);
        this.startLevel = startLevel;
        this.startupMonitor = startupMonitor;

        startupMonitor.addAppStatusChangedListener(this);

        apps = new HashSet<App>();
        appParser = new AppParser();
        appListeners = new HashSet<AppsChangedListener>();

        // cleanKarafDeployDirectory();
        purgeTemporaryDirectories();
        initializeAppsDirectories();

        attemptInitialization();
    }

    public void setAppManagerDialog(AppManagerDialog dialog) {
        this.appManagerDialog = dialog;
    }

    public AppManagerDialog getAppManagerDialog() {
        return appManagerDialog;
    }

    @Override
    public void handleAppStatusChanged(String symbolicName, String version, AppStatus status) {
        for (App app : apps) {
            if (app.getAppName().equals(symbolicName)
                    && WebQuerier.compareVersions(app.getVersion(), version) == 0) {
                app.setStatus(status);
                fireAppsChangedEvent();
                break;
            }
        }
    }

    @Override
    public void frameworkEvent(FrameworkEvent event) {
        // Defer initialization until we reach the right start level.
        if (event.getType() == FrameworkEvent.STARTLEVEL_CHANGED) {
            attemptInitialization();
        }
    }

    void attemptInitialization() {
        synchronized (this) {
            if (!isInitialized && startLevel.getStartLevel() >= APP_START_LEVEL) {
                startupMonitor.setActive(true);
                initializeApps();
                isInitialized = true;
            }
        }
    }

    void initializeApps() {
        // Move apps from install-on-restart directory to install directory
        Set<App> installOnRestartApps = obtainAppsFromDirectory(new File(getInstallOnRestartAppsPath()), false);
        for (App app : installOnRestartApps) {
            try {
                app.moveAppFile(this, new File(getInstalledAppsPath()));
            } catch (IOException e) {
            }
        }

        // Remove the install-on-restart directory after apps were moved
        try {
            FileUtils.deleteDirectory(new File(getInstallOnRestartAppsPath()));
        } catch (IOException e) {
        }

        setupAlterationMonitor();

        // Obtain previously disabled, installed apps

        Set<App> disabledFolderApps = obtainAppsFromDirectory(new File(getDisabledAppsPath()), false);
        for (App app : disabledFolderApps) {
            try {
                boolean appRegistered = false;
                for (App regApp : apps) {
                    if (regApp.heuristicEquals(app))
                        appRegistered = true;
                }
                if (!appRegistered) {
                    apps.add(app);
                    app.disable(this);
                } else {
                    // Delete the copy
                    FileUtils.deleteQuietly(app.getAppFile());
                    app.setAppFile(null);
                }
            } catch (Throwable e) {
            }
        }

        Set<App> uninstalledFolderApps = obtainAppsFromDirectory(new File(getUninstalledAppsPath()), false);
        for (App app : uninstalledFolderApps) {
            try {
                boolean appRegistered = false;
                for (App regApp : apps) {
                    if (regApp.heuristicEquals(app))
                        appRegistered = true;
                }
                if (!appRegistered) {
                    apps.add(app);
                    app.uninstall(this);
                } else {
                    // Delete the copy
                    FileUtils.deleteQuietly(app.getAppFile());
                    app.setAppFile(null);
                }
            } catch (Throwable e) {
            }
        }

        Set<App> installedFolderApps = obtainAppsFromDirectory(new File(getInstalledAppsPath()), false);
        for (App app : installedFolderApps) {
            try {
                boolean appRegistered = false;
                for (App regApp : apps) {
                    if (regApp.heuristicEquals(app))
                        appRegistered = true;
                }
                if (!appRegistered) {
                    apps.add(app);
                    app.install(this);
                } else {
                    // Delete the copy
                    FileUtils.deleteQuietly(app.getAppFile());
                    app.setAppFile(null);
                }
            } catch (Throwable e) {
                logger.warn("Failed to initially install app, " + e);
            }
        }

        DebugHelper.print(this, "config dir: " + applicationConfiguration.getConfigurationDirectoryLocation());
    }

    private void setupAlterationMonitor() {
        // Set up the FileAlterationMonitor to install/uninstall apps when apps are moved in/out of the 
        // installed/uninstalled app directories
        fileAlterationMonitor = new FileAlterationMonitor(2000L);

        File installedAppsPath = new File(getInstalledAppsPath());

        FileAlterationObserver installAlterationObserver = new FileAlterationObserver(installedAppsPath,
                new SingleLevelFileFilter(installedAppsPath), IOCase.SYSTEM);

        final AppManager appManager = this;

        // Listen for events on the "installed apps" folder
        installAlterationObserver.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileCreate(File file) {
                App parsedApp = null;
                try {
                    parsedApp = appParser.parseApp(file);
                } catch (AppParsingException e) {
                    return;
                }

                App registeredApp = null;
                for (App app : apps) {
                    if (parsedApp.heuristicEquals(app)) {
                        registeredApp = app;

                        // Delete old file if it was still there
                        File oldFile = registeredApp.getAppFile();

                        if (oldFile.exists() && !registeredApp.getAppFile().equals(parsedApp.getAppFile())) {
                            FileUtils.deleteQuietly(oldFile);
                        }

                        // Update file reference to reflect file having been moved
                        registeredApp.setAppFile(file);
                    }
                }

                try {
                    if (registeredApp == null) {
                        apps.add(parsedApp);
                        parsedApp.install(appManager);
                    } else {
                        registeredApp.install(appManager);
                    }
                } catch (AppInstallException e) {
                    logger.warn(e.getLocalizedMessage());
                }

                fireAppsChangedEvent();
            }

            @Override
            public void onFileChange(File file) {
                // Can treat file replacements/changes as old file deleted, new file added
                this.onFileDelete(file);
                this.onFileCreate(file);

                fireAppsChangedEvent();
            }

            @Override
            public void onFileDelete(File file) {
                // System.out.println(file + " on delete");

                DebugHelper.print(this + " installObserverDelete", file.getAbsolutePath() + " deleted.");

                for (App app : apps) {

                    if (app.getAppFile().equals(file)) {
                        app.setStatus(AppStatus.FILE_MOVED);
                    }
                }

                fireAppsChangedEvent();
            }
        });

        FileAlterationObserver disableAlterationObserver = new FileAlterationObserver(getDisabledAppsPath(),
                new SingleLevelFileFilter(new File(getDisabledAppsPath())), IOCase.SYSTEM);

        // Listen for events on the "disabled apps" folder
        disableAlterationObserver.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileCreate(File file) {
                App parsedApp = null;
                try {
                    parsedApp = appParser.parseApp(file);
                } catch (AppParsingException e) {
                    logger.warn(e.getLocalizedMessage());
                    return;
                }

                DebugHelper.print(this + " disableObserver Create", parsedApp.getAppName() + " parsed");

                App registeredApp = null;
                for (App app : apps) {
                    if (parsedApp.heuristicEquals(app)) {
                        registeredApp = app;

                        // Delete old file if it was still there
                        // TODO: Possible rename from filename-2 to filename?
                        File oldFile = registeredApp.getAppFile();

                        if (oldFile.exists() && !registeredApp.getAppFile().equals(parsedApp.getAppFile())) {
                            DebugHelper.print(this + " disableObserverCreate",
                                    registeredApp.getAppName() + " moved from "
                                            + registeredApp.getAppFile().getAbsolutePath() + " to "
                                            + parsedApp.getAppFile().getAbsolutePath() + ". deleting: " + oldFile);

                            FileUtils.deleteQuietly(oldFile);
                        }

                        // Update file reference to reflect file having been moved
                        registeredApp.setAppFile(file);
                    }
                }

                try {
                    if (registeredApp == null) {
                        apps.add(parsedApp);
                        parsedApp.disable(appManager);
                    } else {
                        registeredApp.disable(appManager);
                    }

                    fireAppsChangedEvent();

                } catch (AppDisableException e) {
                }

                // System.out.println(file + " on create");
            }

            @Override
            public void onFileChange(File file) {
                // Can treat file replacements/changes as old file deleted, new file added
                this.onFileDelete(file);
                this.onFileCreate(file);

                fireAppsChangedEvent();
            }

            @Override
            public void onFileDelete(File file) {
                // System.out.println(file + " on delete");

                DebugHelper.print(this + " disableObserverDelete", file.getAbsolutePath() + " deleted.");

                for (App app : apps) {
                    // System.out.println("checking " + app.getAppFile().getAbsolutePath());
                    if (app.getAppFile().equals(file)) {
                        // System.out.println(app + " moved");
                        app.setStatus(AppStatus.FILE_MOVED);
                    }
                }

                fireAppsChangedEvent();
            }
        });

        FileAlterationObserver uninstallAlterationObserver = new FileAlterationObserver(getUninstalledAppsPath(),
                new SingleLevelFileFilter(new File(getUninstalledAppsPath())), IOCase.SYSTEM);

        // Listen for events on the "uninstalled apps" folder
        uninstallAlterationObserver.addListener(new FileAlterationListenerAdaptor() {
            @Override
            public void onFileCreate(File file) {
                App parsedApp = null;
                try {
                    parsedApp = appParser.parseApp(file);
                } catch (AppParsingException e) {
                    return;
                }

                DebugHelper.print(this + " uninstallObserverCreate", parsedApp.getAppName() + " parsed");

                App registeredApp = null;
                for (App app : apps) {
                    if (parsedApp.heuristicEquals(app)) {
                        registeredApp = app;

                        // Delete old file if it was still there
                        // TODO: Possible rename from filename-2 to filename?
                        File oldFile = registeredApp.getAppFile();

                        if (oldFile.exists() && !registeredApp.getAppFile().equals(parsedApp.getAppFile())) {
                            DebugHelper.print(this + " uninstallObserverCreate",
                                    registeredApp.getAppName() + " moved from "
                                            + registeredApp.getAppFile().getAbsolutePath() + " to "
                                            + parsedApp.getAppFile().getAbsolutePath() + ". deleting: " + oldFile);

                            FileUtils.deleteQuietly(oldFile);
                        }

                        // Update file reference to reflect file having been moved
                        registeredApp.setAppFile(file);
                    }
                }

                try {
                    // Checks if the app file moved here belonged to a known app, if so, uninstall it.
                    if (registeredApp == null) {
                        apps.add(parsedApp);
                        parsedApp.uninstall(appManager);
                    } else {
                        registeredApp.uninstall(appManager);
                    }

                    fireAppsChangedEvent();

                } catch (AppUninstallException e) {
                }

                // System.out.println(file + " on create");
            }

            @Override
            public void onFileChange(File file) {
                // Can treat file replacements/changes as old file deleted, new file added
                this.onFileDelete(file);
                this.onFileCreate(file);

                fireAppsChangedEvent();
            }

            @Override
            public void onFileDelete(File file) {
                // System.out.println(file + " on delete");

                DebugHelper.print(this + " uninstallObserverDelete", file.getAbsolutePath() + " deleted.");

                for (App app : apps) {
                    // System.out.println("checking " + app.getAppFile().getAbsolutePath());
                    if (app.getAppFile().equals(file)) {
                        // System.out.println(app + " moved");
                        app.setStatus(AppStatus.FILE_MOVED);
                    }
                }

                fireAppsChangedEvent();
            }
        });

        // setupKarafDeployMonitor(fileAlterationMonitor);

        try {
            installAlterationObserver.initialize();
            fileAlterationMonitor.addObserver(installAlterationObserver);

            disableAlterationObserver.initialize();
            fileAlterationMonitor.addObserver(disableAlterationObserver);

            uninstallAlterationObserver.initialize();
            fileAlterationMonitor.addObserver(uninstallAlterationObserver);

            fileAlterationMonitor.start();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private void checkForFileChanges() {
        for (FileAlterationObserver observer : fileAlterationMonitor.getObservers()) {
            observer.checkAndNotify();
        }
    }

    public CySwingAppAdapter getSwingAppAdapter() {
        return swingAppAdapter;
    }

    public AppParser getAppParser() {
        return appParser;
    }

    public WebQuerier getWebQuerier() {
        return webQuerier;
    }

    /**
     * Registers an app to this app manager.
     * @param app The app to register to this manager.
     */
    public void addApp(App app) {
        apps.add(app);

        /*
        // Let the listeners know that an app has changed
        for (AppsChangedListener appListener : appListeners) {
           AppsChangedEvent appEvent = new AppsChangedEvent(this);
           appListener.appsChanged(appEvent);
        }
        */
    }

    /**
     * Removes an app from this app manager.
     * @param app The app to remove
     */
    public void removeApp(App app) {
        apps.remove(app);
    }

    /**
     * Attempts to install an app. Makes a copy of the app file and places it in the directory 
     * used to hold all installed and uninstalled apps, if it was not already present there. Then, the 
     * app is created by instancing its class that extends {@link AbstractCyApp}.
     * 
     * Before the app is installed, it is checked if it contains valid packaging by its isAppValidated() method.
     * Apps that have not been validated are ignored. Also, apps that are already installed are left alone.
     * 
     * @param app The {@link App} object representing and providing information about the app to install
     * @throws AppInstallException If there was an error while attempting to install the app such as being
     * unable to copy the app to the installed apps directory or to instance the app's entry point class
     */
    public void installApp(App app) throws AppInstallException {

        /*
        try {
           System.out.println(getChecksum(app.getAppFile()));
        } catch (ChecksumException e) {
           System.out.println(e.getMessage());
        }
        */

        try {
            app.moveAppFile(this, new File(getInstalledAppsPath()));
        } catch (IOException e) {
            throw new AppInstallException("Unable to move app file, " + e.getMessage());
        }

        checkForFileChanges();
    }

    /**
     * Uninstalls an app. If it was located in the subdirectory containing currently installed apps in the
     * local storage directory, it will be moved to the subdirectory containing currently uninstalled apps.
     * 
     * @param app The app to be uninstalled.
     * @throws AppUninstallException If there was an error while attempting to uninstall the app such as
     * attempting to uninstall an app that isn't installed, or being unable to move the app to the uninstalled
     * apps directory
     */
    public void uninstallApp(App app) throws AppUninstallException {

        try {
            app.moveAppFile(this, new File(getUninstalledAppsPath()));
        } catch (IOException e) {
            throw new AppUninstallException("Unable to move app file, " + e.getMessage());
        }

        checkForFileChanges();
    }

    public void disableApp(App app) throws AppDisableException {

        try {
            app.moveAppFile(this, new File(getDisabledAppsPath()));
        } catch (IOException e) {
            throw new AppDisableException("Unable to move app file, " + e.getMessage());
        }

        checkForFileChanges();
    }

    //    /**
    //     * Attempts to remove uninstalled apps so that they don't take up space on the UI.
    //     * If you wish to display uninstalled apps for purposes of completion, avoid calling this function.
    //     */
    //    public void clearUninstalledApps() {
    //       Set<App> appsToBeRemoved = new HashSet<App>();
    //       
    //       for (App app : apps) {
    //          if (app.getStatus() == AppStatus.UNINSTALLED
    //                || app.getStatus() == AppStatus.FILE_MOVED) {
    //             
    //             appsToBeRemoved.add(app);
    //          }
    //       }
    //       
    //       this.apps.removeAll(appsToBeRemoved);
    //    }

    public void fireAppsChangedEvent() {
        AppsChangedEvent appEvent = new AppsChangedEvent(this);
        for (AppsChangedListener appListener : appListeners) {
            appListener.appsChanged(appEvent);
        }
    }

    /**
     * Return the set of all apps registered to this app manager.
     * @return The set of all apps registered to this app manager.
     */
    public Set<App> getApps() {
        return apps;
    }

    /**
     * Return the path of the directory used to contain all apps.
     * @return The path of the root directory containing all installed and uninstalled apps.
     */
    private File getBaseAppPath() {
        File baseAppPath = null;

        // TODO: At time of writing, CyApplicationConfiguration always returns the home directory for directory location.
        try {
            baseAppPath = new File(applicationConfiguration.getConfigurationDirectoryLocation().getCanonicalPath()
                    + File.separator + APPS_DIRECTORY_NAME);
        } catch (IOException e) {
            throw new RuntimeException(
                    "Unabled to obtain canonical path for Cytoscape local storage directory: " + e.getMessage());
        }

        return baseAppPath;
    }

    /**
     * Return the canonical path of the subdirectory in the local storage directory containing installed apps.
     * @return The canonical path of the subdirectory in the local storage directory containing currently installed apps,
     * or <code>null</code> if there was an error obtaining the canonical path.
     */
    public String getInstalledAppsPath() {
        File path = new File(getBaseAppPath() + File.separator + INSTALLED_APPS_DIRECTORY_NAME);

        try {
            // Create the directory if it doesn't exist   
            if (!path.exists()) {
                path.mkdirs();
            }

            return path.getCanonicalPath();
        } catch (IOException e) {
            logger.warn("Failed to obtain path to installed apps directory");
            return path.getAbsolutePath();
        }
    }

    /**
     * Return the canonical path of the subdirectory in the local storage directory containing disabled apps.
     * @return The canonical path of the subdirectory in the local storage directory containing disabled apps,
     * or <code>null</code> if there was an error obtaining the canonical path.
     */
    public String getDisabledAppsPath() {
        File path = new File(getBaseAppPath() + File.separator + DISABLED_APPS_DIRECTORY_NAME);

        try {
            // Create the directory if it doesn't exist   
            if (!path.exists()) {
                path.mkdirs();
            }

            return path.getCanonicalPath();
        } catch (IOException e) {
            logger.warn("Failed to obtain path to disabled apps directory");
            return path.getAbsolutePath();
        }
    }

    /**
     * Return the canonical path of the temporary directory in the local storage directory used to contain apps that
     * are currently loaded.
     * @return The canonical path of the temporary directory containing apps with classes that are loaded.
     */
    public String getTemporaryInstallPath() {
        File path = new File(getBaseAppPath() + File.separator + TEMPORARY_LOADED_APPS_DIRECTORY_NAME);

        try {
            // Create the directory if it doesn't exist
            if (!path.exists()) {
                path.mkdirs();
            }

            return path.getCanonicalPath();
        } catch (IOException e) {
            logger.warn("Failed to obtain canonical path to the temporary installed apps directory");
            return path.getAbsolutePath();
        }
    }

    /**
     * Return the canonical path of the subdirectory in the local storage directory containing uninstalled apps.
     * @return The canonical path of the subdirectory in the local storage directory containing uninstalled apps,
     * or <code>null</code> if there was an error obtaining the canonical path.
     */
    public String getUninstalledAppsPath() {
        File path = new File(getBaseAppPath() + File.separator + UNINSTALLED_APPS_DIRECTORY_NAME);

        try {
            // Create the directory if it doesn't exist
            if (!path.exists()) {
                path.mkdirs();
            }

            return path.getCanonicalPath();
        } catch (IOException e) {
            logger.warn("Failed to obtain path to uninstalled apps directory");
            return path.getAbsolutePath();
        }
    }

    /**
     * Return the canonical path of the subdirectory in the local storage directory used to temporarily store
     * apps downloaded from the app store.
     * @return The canonical path of the subdirectory in the local storage directory temporarily
     * storing apps downloaded from the app store.
     */
    public String getDownloadedAppsPath() {
        File path = new File(getBaseAppPath() + File.separator + DOWNLOADED_APPS_DIRECTORY_NAME);

        try {
            // Create the directory if it doesn't exist
            if (!path.exists()) {
                path.mkdirs();
            }

            return path.getCanonicalPath();
        } catch (IOException e) {
            logger.warn("Failed to obtain path to downloaded apps directory");
            return path.getAbsolutePath();
        }
    }

    /**
     * Return the canonical path of the subdirectory in the local storage directory used to contain apps that
     * are installed on restart.
     * @return The canonical path of the subdirectory in the local storage directory containing
     * apps to install on restart.
     */
    public String getInstallOnRestartAppsPath() {
        File path = new File(getBaseAppPath() + File.separator + INSTALL_RESTART_DIRECTORY_NAME);

        try {
            // Create the directory if it doesn't exist
            if (!path.exists()) {
                path.mkdirs();
            }

            return path.getCanonicalPath();
        } catch (IOException e) {
            logger.warn("Failed to obtain path to directory containing apps to install on restart");
            return path.getAbsolutePath();
        }
    }

    private boolean checkIfCytoscapeApp(File file) {
        JarFile jarFile = null;

        try {
            jarFile = new JarFile(file);

            Manifest manifest = jarFile.getManifest();

            // Check the manifest file 
            if (manifest != null) {
                if (manifest.getMainAttributes().getValue("Cytoscape-App-Name") != null) {

                    jarFile.close();
                    return true;
                }
            }

            jarFile.close();
        } catch (ZipException e) {
            // Do nothing; skip file
            // e.printStackTrace();
        } catch (IOException e) {
            // Do nothing; skip file
            // e.printStackTrace();
        } finally {
            if (jarFile != null) {
                try {
                    jarFile.close();
                } catch (IOException e) {
                }
            }
        }

        return false;
    }

    /**
     * Removes the temporary app download directory and the directory used to store uninstalled apps.
     */
    public void purgeTemporaryDirectories() {
        File downloaded = new File(getDownloadedAppsPath());
        File uninstalled = new File(getUninstalledAppsPath());
        File temporaryInstall = new File(getTemporaryInstallPath());

        try {
            FileUtils.deleteDirectory(downloaded);
            FileUtils.deleteDirectory(uninstalled);
            FileUtils.deleteDirectory(temporaryInstall);
        } catch (IOException e) {
            logger.warn(
                    "Unable to completely remove temporary directories for downloaded, loaded, and uninstalled apps.");
        }
    }

    /**
     * Obtain a set of {@link App} objects through attempting to parse files found in the first level of the given directory.
     * @param directory The directory used to parse {@link App} objects
     * @return A set of all {@link App} objects that were successfully parsed from files in the given directory
     */
    private Set<App> obtainAppsFromDirectory(File directory, boolean ignoreDuplicateBundleApps) {
        // Obtain all files in the given directory with supported extensions, perform a non-recursive search
        Collection<File> files = FileUtils.listFiles(directory, APP_EXTENSIONS, false);

        Set<App> parsedApps = new HashSet<App>();

        App app;
        for (File potentialApp : files) {

            app = null;
            try {
                app = appParser.parseApp(potentialApp);
            } catch (AppParsingException e) {
                DebugHelper.print("Failed to parse " + potentialApp + ", error: " + e.getMessage());
                app = null;
            } finally {
                if (app != null) {
                    parsedApps.add(app);

                    DebugHelper.print("App parsed: " + app);
                }
            }
        }

        return parsedApps;
    }

    /**
     * Create app storage directories if they don't already exist.
     */
    private void initializeAppsDirectories() {
        boolean created = true;

        File appDirectory = getBaseAppPath();
        if (!appDirectory.exists()) {
            created = created && appDirectory.mkdirs();
            logger.info("Creating " + appDirectory + ". Success? " + created);
        }

        File installedDirectory = new File(getInstalledAppsPath());
        if (!installedDirectory.exists()) {
            created = created && installedDirectory.mkdirs();
            logger.info("Creating " + installedDirectory + ". Success? " + created);
        }

        File disabledDirectory = new File(getDisabledAppsPath());
        if (!disabledDirectory.exists()) {
            created = created && disabledDirectory.mkdirs();
            logger.info("Creating " + disabledDirectory + ". Success? " + created);
        }

        File temporaryInstallDirectory = new File(getTemporaryInstallPath());
        if (!temporaryInstallDirectory.exists()) {
            created = created && temporaryInstallDirectory.mkdirs();
            logger.info("Creating " + temporaryInstallDirectory + ". Success? " + created);
        }

        File uninstalledDirectory = new File(getUninstalledAppsPath());
        if (!uninstalledDirectory.exists()) {
            created = created && uninstalledDirectory.mkdirs();
            logger.info("Creating " + uninstalledDirectory + ". Success? " + created);
        }

        File downloadedDirectory = new File(getDownloadedAppsPath());
        if (!downloadedDirectory.exists()) {
            created = created && downloadedDirectory.mkdirs();
        }

        File installRestartDirectory = new File(getInstallOnRestartAppsPath());
        if (!installRestartDirectory.exists()) {
            created = created && installRestartDirectory.mkdirs();
            logger.info("Creating " + installRestartDirectory + ". Success? " + created);
        }

        if (!created) {
            logger.error("Failed to create local app storage directories.");
        }
    }

    public void addAppListener(AppsChangedListener appListener) {
        appListeners.add(appListener);
    }

    public void removeAppListener(AppsChangedListener appListener) {
        appListeners.remove(appListener);
    }
}