/*
* 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().logError(
"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(" ");
}
}
}