com.amazonaws.eclipse.dynamodb.testtool.TestToolManager.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.eclipse.dynamodb.testtool.TestToolManager.java

Source

/*
 * Copyright 2013 Amazon Technologies, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *    http://aws.amazon.com/apache2.0
 *
 * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and
 * limitations under the License.
 */
package com.amazonaws.eclipse.dynamodb.testtool;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.environments.IExecutionEnvironment;
import org.eclipse.jdt.launching.environments.IExecutionEnvironmentsManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.eclipse.core.AWSClientFactory;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.regions.RegionUtils;
import com.amazonaws.eclipse.core.regions.ServiceAbbreviations;
import com.amazonaws.eclipse.dynamodb.DynamoDBPlugin;
import com.amazonaws.eclipse.dynamodb.preferences.TestToolPreferencePage;
import com.amazonaws.eclipse.dynamodb.testtool.TestToolVersion.InstallState;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.transfer.Download;
import com.amazonaws.services.s3.transfer.TransferManager;

/**
 * The singleton manager for DynamoDB Local Test Tool instances.
 */
public class TestToolManager {

    public static final TestToolManager INSTANCE = new TestToolManager();

    private static final String TEST_TOOL_BUCKET = "aws-toolkits-dynamodb-local";

    private static final String TEST_TOOL_MANIFEST = "manifest.xml";

    private static final Pattern WHITESPACE = Pattern.compile("\\s+");

    private final Set<String> installing = Collections.synchronizedSet(new HashSet<String>());

    private TransferManager transferManager;
    private List<TestToolVersion> versions;

    private TestToolVersion currentlyRunningVersion;
    private TestToolProcess currentProcess;

    private TestToolManager() {
    }

    /**
     * Get a list of all versions of the DynamoDB Local Test Tool which can
     * be installed on the system. The first time this method is called, it
     * attempts to pull the list of versions from S3 (falling back to an
     * on-disk cache in case of failure). On subsequent calls it returns the
     * in-memory cache (updated to reflect the current state of what is and
     * is not installed).
     *
     * @return the list of all local test tool versions
     */
    public synchronized List<TestToolVersion> getAllVersions() {
        if (versions == null) {
            versions = loadVersions();
        } else {
            versions = refreshInstallStates(versions);
        }
        return versions;
    }

    /**
     * Determines whether a Java 7 compatible JRE exists on the system.
     *
     * @return true if a Java 7 compatible JRE is found, false otherwise
     */
    public boolean isJava7Available() {
        return (getJava7VM() != null);
    }

    /**
     * Gets a Java 7 compatible VM, if one can be found.
     *
     * @return  the VM install, or null if none was found
     */
    private IVMInstall getJava7VM() {
        IExecutionEnvironmentsManager manager = JavaRuntime.getExecutionEnvironmentsManager();

        IExecutionEnvironment environment = manager.getEnvironment("JavaSE-1.7");
        if (environment == null) {
            // This version of Eclipse doesn't even know that Java 7 exists.
            return null;
        }

        IVMInstall defaultVM = environment.getDefaultVM();
        if (defaultVM != null) {
            // If the user has set a default VM to use for Java 7, go with
            // that.
            return defaultVM;
        }

        IVMInstall[] installs = environment.getCompatibleVMs();
        if (installs != null && installs.length > 0) {
            // Otherwise just pick the latest compatible VM.
            return installs[installs.length - 1];
        }

        // No compatible VMs installed.
        return null;
    }

    /**
     * Set the install state of the given instance to INSTALLING, so that
     * we disable both the install and uninstall buttons in the UI.
     *
     * @param version The version to mark as INSTALLING.
     */
    public synchronized void markInstalling(final TestToolVersion version) {
        if (!version.isInstalled()) {
            installing.add(version.getName());
        }
    }

    /**
     * Install the given version of the test tool.
     *
     * @param version The version of the test tool to install.
     * @param monitor A progress monitor to keep updated.
     */
    public void installVersion(final TestToolVersion version, final IProgressMonitor monitor) {

        if (version.isInstalled()) {
            return;
        }

        try {

            File tempFile = File.createTempFile("dynamodb_local_", "");
            tempFile.delete();
            if (!tempFile.mkdirs()) {
                throw new RuntimeException("Failed to create temporary " + "directory for download");
            }

            File zipFile = new File(tempFile, "dynamodb_local.zip");

            download(version.getDownloadKey(), zipFile, monitor);

            File unzipped = new File(tempFile, "unzipped");
            if (!unzipped.mkdirs()) {
                throw new RuntimeException("Failed to create temporary " + "directory for unzipping");
            }

            unzip(zipFile, unzipped);

            File versionDir = getVersionDirectory(version.getName());
            FileUtils.copyDirectory(unzipped, versionDir);

        } catch (IOException exception) {
            throw new RuntimeException("Error installing DynamoDB Local: " + exception.getMessage(), exception);
        } finally {
            monitor.done();
            installing.remove(version.getName());
        }
    }

    /**
     * @return  true if a local test tool process is currently running
     */
    public synchronized boolean isRunning() {
        return (currentProcess != null);
    }

    /**
     * @return  the port to which the current process is bound (or null if no
     *          process is currently running)
     */
    public synchronized Integer getCurrentPort() {
        if (currentProcess == null) {
            return null;
        }
        return currentProcess.getPort();
    }

    /**
     * Start the given version of the DynamoDBLocal test tool.
     *
     * @param version   the version of the test tool to start
     * @param port      the port to bind to
     */
    public synchronized void startVersion(final TestToolVersion version, final int port) {

        if (!version.isInstalled()) {
            throw new IllegalStateException("Cannot start a version which is " + "not installed.");
        }

        // We should have cleaned this up already, but just to be safe...
        if (currentProcess != null) {
            currentProcess.stop();
            currentProcess = null;
            currentlyRunningVersion = null;
        }

        IVMInstall jre = getJava7VM();
        if (jre == null) {
            throw new IllegalStateException("No Java 7 VM found!");
        }

        try {

            File installDirectory = getVersionDirectory(version.getName());
            final TestToolProcess process = new TestToolProcess(jre, installDirectory, port);

            currentProcess = process;
            currentlyRunningVersion = version;

            RegionUtils.addLocalService(ServiceAbbreviations.DYNAMODB, "dynamodb", port);

            // If the process dies for some reason other than that we killed
            // it, clear out our internal state so the user can start another
            // instance.
            process.start(new Runnable() {
                public void run() {
                    synchronized (TestToolManager.this) {
                        if (process == currentProcess) {
                            cleanUpProcess();
                        }
                    }
                }
            });

        } catch (IOException exception) {
            throw new RuntimeException("Error starting the DynamoDB Local Test Tool: " + exception.getMessage(),
                    exception);
        }

    }

    /**
     * Stop the currently-running DynamoDBLocal process.
     */
    public synchronized void stopVersion() {
        if (currentProcess != null) {
            currentProcess.stop();
            cleanUpProcess();
        }
    }

    private void cleanUpProcess() {
        currentProcess = null;
        currentlyRunningVersion = null;

        // Revert to a default port setting.

        DynamoDBPlugin.getDefault().setDefaultDynamoDBLocalPort();
    }

    /**
     * Download the given object from S3 to the given file, updating the
     * given progress monitor periodically.
     *
     * @param key The key of the object to download.
     * @param destination The destination file to download to.
     * @param monitor The progress monitor to update.
     */
    private void download(final String key, final File destination, final IProgressMonitor monitor) {
        try {

            TransferManager tm = getTransferManager();
            Download download = tm.download(TEST_TOOL_BUCKET, key, destination);

            int totalWork = (int) download.getProgress().getTotalBytesToTransfer();
            monitor.beginTask("Downloading DynamoDB Local", totalWork);

            int worked = 0;
            while (!download.isDone()) {
                int bytes = (int) download.getProgress().getBytesTransferred();
                if (bytes > worked) {
                    int newWork = bytes - worked;
                    monitor.worked(newWork);
                    worked = bytes;
                }
                Thread.sleep(500);
            }

        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted while installing DynamoDB Local", exception);
        } catch (AmazonServiceException exception) {
            throw new RuntimeException("Error downloading DynamoDB Local: " + exception.getMessage(), exception);
        }
    }

    /**
     * Unzip the given file into the given directory.
     *
     * @param zipFile The zip file to unzip.
     * @param unzipped The directory to put the unzipped files into.
     * @throws IOException on file system error.
     */
    private void unzip(final File zipFile, final File unzipped) throws IOException {

        ZipInputStream zip = new ZipInputStream(new FileInputStream(zipFile));
        try {

            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                Path path = new Path(entry.getName());

                File dest = new File(unzipped, path.toOSString());
                if (entry.isDirectory()) {
                    if (!dest.mkdirs()) {
                        throw new RuntimeException("Failed to create directory while unzipping");
                    }
                } else {
                    FileOutputStream output = new FileOutputStream(dest);
                    try {
                        IOUtils.copy(zip, output);
                    } finally {
                        output.close();
                    }
                }
            }

        } finally {
            zip.close();
        }
    }

    /**
     * Uninstall the given version of the test tool.
     *
     * @param version The version to uninstall.
     */
    public void uninstallVersion(final TestToolVersion version) {
        if (!version.isInstalled()) {
            return;
        }

        try {
            FileUtils.deleteDirectory(getVersionDirectory(version.getName()));
        } catch (IOException exception) {
            throw new RuntimeException("Error while uninstalling DynamoDB Local: " + exception.getMessage(),
                    exception);
        }
    }

    /**
     * Load the set of test tool versions from S3, falling back to a cache
     * on the local disk in case of error.
     *
     * @return The loaded test tool version list.
     */
    private List<TestToolVersion> loadVersions() {
        try {

            return loadVersionsFromS3();

        } catch (IOException exception) {
            try {

                return loadVersionsFromLocalCache();

            } catch (IOException e) {
                // No local cache; throw the original exception.
                throw new RuntimeException("Error loading DynamoDB Local Test Tool version manifest "
                        + "from Amazon S3. Are you connected to the Internet?", exception);
            }
        }
    }

    /**
     * Loop through the existing set of test tool versions and build a new
     * list with install states updated to reflect what's actually on disk.
     *
     * @param previous The existing list of versions.
     * @return The updated list of versions.
     */
    private List<TestToolVersion> refreshInstallStates(final List<TestToolVersion> previous) {
        List<TestToolVersion> rval = new ArrayList<TestToolVersion>(previous.size());

        for (TestToolVersion version : previous) {
            InstallState installState = getInstallState(version.getName());

            if (currentlyRunningVersion != null && currentlyRunningVersion.getName().equals(version.getName())) {
                installState = InstallState.RUNNING;
            } else if (installing.contains(version.getName())) {
                installState = InstallState.INSTALLING;
            }

            if (installState == version.getInstallState()) {
                rval.add(version);
            } else {
                rval.add(new TestToolVersion(version.getName(), version.getDescription(), version.getDownloadKey(),
                        installState));
            }
        }

        return rval;
    }

    /**
     * Attempt to load the manifest file describing available versions of the
     * test tool from S3. On success, update our local cache of the manifest
     * file.
     *
     * @return The list of versions.
     * @throws IOException on error.
     */
    private List<TestToolVersion> loadVersionsFromS3() throws IOException {

        File tempFile = File.createTempFile("dynamodb_local_manifest_", ".xml");
        tempFile.delete();

        TransferManager manager = getTransferManager();
        try {
            Download download = manager.download(TEST_TOOL_BUCKET, TEST_TOOL_MANIFEST, tempFile);

            download.waitForCompletion();
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted while downloading DynamoDB Local version manifest " + "from S3",
                    exception);
        } catch (AmazonServiceException exception) {
            throw new IOException("Error downloading DynamoDB Local manifest " + "from S3", exception);
        }

        List<TestToolVersion> rval = parseManifest(tempFile);

        try {
            FileUtils.copyFile(tempFile, getLocalManifestFile());
        } catch (IOException exception) {
            AwsToolkitCore.getDefault()
                    .logException("Error caching manifest file to local disk; do you have "
                            + "write permission to the configured install directory? " + exception.getMessage(),
                            exception);
        }

        return rval;
    }

    /**
     * Attempt to load the list of test tool versions from local cache.
     *
     * @return The list of test tool versions.
     * @throws IOException on error.
     */
    private List<TestToolVersion> loadVersionsFromLocalCache() throws IOException {

        return parseManifest(getLocalManifestFile());
    }

    /**
     * Parse a manifest file describing a list of test tool versions.
     *
     * @param file The file to parse.
     * @return The parsed list of versions.
     * @throws IOException on error.
     */
    private List<TestToolVersion> parseManifest(final File file) throws IOException {

        FileInputStream stream = null;
        try {

            stream = new FileInputStream(file);
            BufferedReader buffer = new BufferedReader(new InputStreamReader(stream));

            ManifestContentHandler handler = new ManifestContentHandler();

            XMLReader reader = XMLReaderFactory.createXMLReader();
            reader.setContentHandler(handler);
            reader.setErrorHandler(handler);
            reader.parse(new InputSource(buffer));

            return handler.getResult();

        } catch (SAXException exception) {
            throw new IOException("Error parsing DynamoDB Local manifest file", exception);
        } finally {
            if (stream != null) {
                stream.close();
            }
        }
    }

    /**
     * Lazily initialize a transfer manager; keep it around to reuse in the
     * future if need be.
     *
     * @return The transfer manager.
     */
    private synchronized TransferManager getTransferManager() {
        if (transferManager == null) {
            AmazonS3 client = AWSClientFactory.getAnonymousS3Client();

            transferManager = new TransferManager(client);
        }
        return transferManager;
    }

    /**
     * @return The path to the file where we'll store the local manifest cache.
     * @throws IOException on error.
     */
    private static File getLocalManifestFile() throws IOException {
        return new File(getInstallDirectory(), "manifest.xml");
    }

    /**
     * Get the install state of a particular version by looking for the
     * presence of the DynamoDBLocal.jar file in the corresponding directory.
     *
     * @param version The version to check for.
     * @return The install state of the given version.
     */
    private static InstallState getInstallState(final String version) {
        try {

            File versionDir = getVersionDirectory(version);

            if (!versionDir.exists()) {
                return InstallState.NOT_INSTALLED;
            }
            if (!versionDir.isDirectory()) {
                return InstallState.NOT_INSTALLED;
            }

            File jar = new File(versionDir, "DynamoDBLocal.jar");
            if (!jar.exists()) {
                return InstallState.NOT_INSTALLED;
            }

            return InstallState.INSTALLED;

        } catch (IOException exception) {
            return InstallState.NOT_INSTALLED;
        }
    }

    /**
     * Get the path to the directory where we would install the given version
     * of the test tool.
     *
     * @param version The version in question.
     * @return The path to the install directory.
     * @throws IOException on error.
     */
    private static File getVersionDirectory(final String version) throws IOException {

        return new File(getInstallDirectory(), version);
    }

    /**
     * Get the path to the root install directory as configured in the test
     * tool preference page.
     *
     * @return The path to the root install directory.
     * @throws IOException on error.
     */
    private static File getInstallDirectory() throws IOException {
        IPreferenceStore preferences = DynamoDBPlugin.getDefault().getPreferenceStore();

        String directory = preferences.getString(TestToolPreferencePage.DOWNLOAD_DIRECTORY_PREFERENCE_NAME);

        File installDir = new File(directory);

        if (!installDir.exists()) {
            if (!installDir.mkdirs()) {
                throw new IOException("Could not create install directory: " + installDir.getAbsolutePath());
            }
        } else {
            if (!installDir.isDirectory()) {
                throw new IOException(
                        "Configured install directory is " + "not a directory: " + installDir.getAbsolutePath());
            }
        }

        return installDir;
    }

    /**
     * SAX handler for the local test tool manifest format.
     */
    private static class ManifestContentHandler extends DefaultHandler {
        private final List<TestToolVersion> versions = new ArrayList<TestToolVersion>();

        private StringBuilder currText = new StringBuilder();

        private String name;
        private String description;
        private String downloadKey;

        /**
         * @return The loaded list of versions.
         */
        public List<TestToolVersion> getResult() {
            return versions;
        }

        @Override
        public void startElement(final String uri, final String localName, final String qName,
                final Attributes attributes) {

            if (localName.equals("version")) {
                // Null these out to be safe.
                name = null;
                description = null;
                downloadKey = null;
            }
        }

        /** {@inheritDoc} */
        @Override
        public void endElement(final String uri, final String localName, final String qName) {

            if (localName.equals("name")) {
                name = trim(currText.toString());
                currText = new StringBuilder();
            } else if (localName.equals("description")) {
                description = trim(currText.toString());
                currText = new StringBuilder();
            } else if (localName.equals("key")) {
                downloadKey = trim(currText.toString());
                currText = new StringBuilder();
            } else if (localName.equals("version")) {
                if (name != null || downloadKey != null) {
                    // Skip versions with no name or download key to be safe.
                    versions.add(new TestToolVersion(name, description, downloadKey, getInstallState(name)));
                }
                name = null;
                description = null;
                downloadKey = null;
            }
        }

        /** {@inheritDoc} */
        @Override
        public void characters(final char[] ch, final int start, final int length) {

            currText.append(ch, start, length);
        }

        /**
         * Trim excess whitespace out of the given string.
         *
         * @param value The value to trim.
         * @return The trimmed value.
         */
        private String trim(final String value) {
            return WHITESPACE.matcher(value.trim()).replaceAll(" ");
        }
    }

}