/*
* Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. 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.sdk.ui;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
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.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.ui.progress.IProgressConstants;
import org.eclipse.ui.statushandlers.StatusManager;
import com.amazonaws.AmazonClientException;
import com.amazonaws.eclipse.core.AWSClientFactory;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.Headers;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.transfer.Download;
import com.amazonaws.services.s3.transfer.TransferManager;
/**
* Abstract base class for managing installs of AWS SDKs. Concrete subclasses
* add support for each specific SDK.
*/
public abstract class AbstractSdkManager<X extends AbstractSdkInstall> {
private SdkDownloadJob installationJob = null;
private final String cloudfrontDownloadUrl;
private final String sdkBucketName;
private final String sdkFilenamePrefix;
private final String initializingSdkJobName;
private Pattern sdkFilenameVersionPattern;
private final SdkInstallFactory<X> sdkInstallFactory;
/**
* Constructs a new SDK manager that can be used to list SDK installs and
* install new versions.
*
* @param sdkName
* The public name of the SDK this SDK manager works with.
* @param sdkBucketName
* The name of the Amazon S3 bucket where SDK versions are
* stored.
* @param sdkFilenamePrefix
* The filename prefix (without the version component) of the SDK
* files in S3. For example, 'aws-java-sdk'.
* @param cloudfrontDistroDomain
* The domain for the CloudFront distribution where the SDK is
* hosted, or null if it's not available in CloudFront.
*/
public AbstractSdkManager(String sdkName, String sdkBucketName,
String sdkFilenamePrefix, String cloudfrontDistroDomain,
SdkInstallFactory<X> sdkInstallFactory) {
if (sdkName == null) throw new IllegalArgumentException("No SDK name specified");
if (sdkFilenamePrefix == null) throw new IllegalArgumentException("No SDK filename prefix specified");
this.initializingSdkJobName = "Initializing " + sdkName;
this.sdkBucketName = sdkBucketName;
this.sdkFilenamePrefix = sdkFilenamePrefix;
this.sdkFilenameVersionPattern = Pattern.compile("filename\\s*=\\s*" + sdkFilenamePrefix + "-(.*?)\\.zip");
this.cloudfrontDownloadUrl = cloudfrontDistroDomain + "/latest/"
+ sdkFilenamePrefix + ".zip";
this.sdkInstallFactory = sdkInstallFactory;
}
/**
* Returns the SDK installation of the specified version, or
* <code>null</code> if no such installation exists.
*
* @param version
* The version of the SDK to return.
*
* @return The SDK installation of the specified version, or
* <code>null</code> if no such installation exists.
*/
public AbstractSdkInstall getSdkInstall(String version) {
for ( AbstractSdkInstall sdkInstall : getSdkInstalls() ) {
if ( sdkInstall.getVersion().equals(version) ) {
return sdkInstall;
}
}
return null;
}
/**
* Returns a list of all the existing SDK installations.
*/
public List<X> getSdkInstalls() {
List<X> sdkInstalls = new LinkedList<X>();
try {
File sdkDir = getSDKInstallDir();
if ( sdkDir.exists() && sdkDir.isDirectory() ) {
for ( File versionDir : sdkDir.listFiles() ) {
X sdkInstall = sdkInstallFactory.createSdkInstallFromDisk(versionDir);
if ( sdkInstall.isValidSdkInstall() )
sdkInstalls.add(sdkInstall);
}
}
return sdkInstalls;
} catch ( IllegalStateException e ) {
JavaSdkPlugin.getDefault().getLog()
.log(new Status(Status.WARNING, JavaSdkPlugin.PLUGIN_ID, "No state directory to cache SDK", e));
return sdkInstalls;
}
}
/**
* Returns the default SDK install, which is the latest
* available, or null if no SDKs are available yet.
*
* @return The default SDK install.
*/
public X getDefaultSdkInstall() {
List<X> sdkInstalls = getSdkInstalls();
Collections.sort(sdkInstalls, new LatestVersionComparator());
if (sdkInstalls.size() > 0) return sdkInstalls.get(0);
return null;
}
/**
* Copies the SDK given into the workspace's private state storage for this
* plugin.
*/
private void copySdk(AbstractSdkInstall install, IProgressMonitor monitor) {
monitor.subTask("Copying SDK to workspace metadata");
try {
File sdkDir = getSDKInstallDir();
File versionDir = new File(sdkDir, install.getVersion());
if ( versionDir.exists() && sdkInstallFactory.createSdkInstallFromDisk(versionDir).isValidSdkInstall() )
return;
if ( !versionDir.exists() && !versionDir.mkdirs() )
throw new Exception("Couldn't make SDK directory " + versionDir);
FileUtils.copyDirectory(install.getRootDirectory(), versionDir);
monitor.worked(20);
} catch ( IllegalStateException e ) {
JavaSdkPlugin.getDefault().getLog()
.log(new Status(Status.WARNING, JavaSdkPlugin.PLUGIN_ID, "No state directory to cache SDK", e));
} catch ( Exception e ) {
JavaSdkPlugin.getDefault().getLog().log(new Status(Status.ERROR, JavaSdkPlugin.PLUGIN_ID, e.getMessage(), e));
}
}
/**
* Returns the version number of the latest SDK in the publicly readable S3 bucket.
*/
private String getLatestS3Version(IProgressMonitor monitor) {
monitor.subTask("Checking latest version in S3");
AmazonS3 client = AWSClientFactory.getAnonymousS3Client();
ObjectMetadata objectMetadata = client.getObjectMetadata(sdkBucketName, "latest/" + sdkFilenamePrefix + ".zip");
String filename = (String) objectMetadata.getRawMetadata().get(Headers.CONTENT_DISPOSITION);
Matcher matcher = sdkFilenameVersionPattern.matcher(filename);
if (matcher.find()) {
return matcher.group(1);
}
IStatus status = new Status(IStatus.ERROR, AwsToolkitCore.PLUGIN_ID, "Unable to detect latest plugin version (Content-Disposition: " + filename + ")");
StatusManager.getManager().handle(status, StatusManager.LOG);
return null;
}
/**
* Comparator that sorts SDK installs from most recent version to oldest.
*/
protected final class LatestVersionComparator implements Comparator<AbstractSdkInstall> {
public int compare(AbstractSdkInstall left, AbstractSdkInstall right) {
int[] leftVersion = parseVersion(left.getVersion());
int[] rightVersion = parseVersion(right.getVersion());
int min = Math.min(leftVersion.length, rightVersion.length);
for (int i = 0; i < min; i++) {
if (leftVersion[i] < rightVersion[i]) return 1;
if (leftVersion[i] > rightVersion[i]) return -1;
}
return 0;
}
private int[] parseVersion(String version) {
if (version == null) return new int[0];
String[] components = version.split("\\.");
int[] ints = new int[components.length];
int counter = 0;
for (String component : components) {
ints[counter++] = Integer.parseInt(component);
}
return ints;
}
}
/**
* Returns the installation job if it exists, or null otherwise.
*/
public SdkDownloadJob getInstallationJob() {
return installationJob;
}
/**
* Initializes the set of SDK installs
*/
public void initializeSDKInstalls() {
synchronized ( this ) {
if ( installationJob != null )
return;
installationJob = new SdkDownloadJob();
}
installationJob.schedule();
}
public final class SdkDownloadJob extends Job {
public SdkDownloadJob() {
super(initializingSdkJobName);
this.setUser(true);
this.setProperty(IProgressConstants.ACTION_PROPERTY, getHyperlinkAction());
this.setProperty(IProgressConstants.KEEP_PROPERTY, true);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
monitor.beginTask("Updating SDK (click details to configure)", 101);
String latestSdkVersion = getLatestS3Version(monitor);
if ( getSdkInstall(latestSdkVersion) == null ) {
downloadAndInstallSDK(monitor);
}
} catch ( Exception e ) {
StatusManager.getManager().handle(
new Status(IStatus.ERROR, JavaSdkPlugin.PLUGIN_ID,
"Couldn't download latest SDK", e),
StatusManager.SHOW);
} finally {
monitor.done();
synchronized ( AbstractSdkManager.this ) {
installationJob = null;
}
}
return new Status(IStatus.OK, JavaSdkPlugin.PLUGIN_ID, "Click to configure");
}
}
/**
* Returns the action to associate with this job, which will be represented
* by a clickable link.
*/
protected abstract Action getHyperlinkAction();
/**
* Returns the default sdk install directory for the current workspace. With some
* command-line arguments, this directory will not exist and cannot be
* created, in which case an {@link IllegalStateException} is thrown.
*/
public File getDefaultSDKInstallDir() throws IllegalStateException {
File userHome = new File(System.getProperty("user.home"));
return new File(userHome, sdkFilenamePrefix);
}
/**
* Returns the base directory to install SDKs in
*/
abstract protected File getSDKInstallDir();
/**
* Downloads the latest copy of the SDK from s3 and caches it in the workspace metadata directory
*/
private void downloadAndInstallSDK(IProgressMonitor monitor) throws IOException {
File tempFile = File.createTempFile(sdkFilenamePrefix, "");
tempFile.delete();
tempFile.mkdirs();
File zipFile = new File(tempFile, "sdk.zip");
/*
* 60 units for SDK download
*/
try {
downloadSdkFromCloudFront(zipFile, monitor, 60);
} catch (Exception e) {
JavaSdkPlugin.getDefault().getLog()
.log(new Status(Status.INFO, JavaSdkPlugin.PLUGIN_ID,
"Fall back to S3 download.", e));
downloadSdkFromS3(zipFile, monitor, 60);
}
JavaSdkPlugin.getDefault().getLog()
.log(new Status(Status.INFO, JavaSdkPlugin.PLUGIN_ID,
"SDK download completes. Location: " + zipFile.getAbsolutePath() + ", " +
"File-length: " + zipFile.length()));
/*
* 20 units for unzipping
*/
File unzippedDir = new File(tempFile, "unzipped");
unzipSDK(zipFile, unzippedDir, monitor, 20);
File sdkDir = unzippedDir.listFiles()[0];
AbstractSdkInstall latest = sdkInstallFactory.createSdkInstallFromDisk(sdkDir);
copySdk(latest, monitor);
}
private void downloadSdkFromCloudFront(File destination,
IProgressMonitor monitor, int totalUnitsOfWork)
throws IOException {
if (cloudfrontDownloadUrl == null) {
throw new IllegalStateException("No CloudFront endpoint is provided.");
}
monitor.subTask("Downloading latest SDK from CloudFront");
JavaSdkPlugin.getDefault().getLog()
.log(new Status(Status.INFO, JavaSdkPlugin.PLUGIN_ID,
"Downloading the SDK from CloudFront to location "
+ destination.getAbsolutePath()));
URL sourceUrl = new URL(cloudfrontDownloadUrl);
URLConnection connection = sourceUrl.openConnection();
long totalBytes;
String contentLength = connection.getHeaderField("Content-Length");
try {
totalBytes = Long.parseLong(contentLength);
} catch (NumberFormatException e) {
totalBytes = -1;
}
InputStream input = connection.getInputStream();
try {
FileOutputStream output = new FileOutputStream(destination);
try {
copyWithProgressMonitor(input, output, monitor,
totalUnitsOfWork, totalBytes);
} finally {
IOUtils.closeQuietly(output);
}
} finally {
IOUtils.closeQuietly(input);
}
if ( !destination.exists() ) {
throw new IllegalStateException(
destination.getAbsolutePath() + " does not exist " +
"after the SDK download completes.");
}
}
private static void copyWithProgressMonitor(InputStream input, OutputStream output,
IProgressMonitor monitor, int totalUnitsOfWork, long totalBytes)
throws IOException {
byte[] buffer = new byte[1024 * 8];
int n = 0;
long workedBytes = 0;
int workedUnits = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
workedBytes += n;
if (totalBytes > 0 && workedUnits < totalUnitsOfWork) {
int newWork = (int)(workedBytes * totalUnitsOfWork / (double)totalBytes) - workedUnits;
if (newWork > 0) {
monitor.worked(newWork);
workedUnits += newWork;
}
}
}
if (workedBytes != totalBytes) {
throw new IllegalStateException(
String.format(
"Data length (%d bytes) doesn't match the content-length (%d bytes).",
workedBytes, totalBytes));
}
if (workedUnits < totalUnitsOfWork) {
monitor.worked(totalUnitsOfWork - workedUnits);
}
}
private void downloadSdkFromS3(File destination, IProgressMonitor monitor, int totalUnitsOfWork) {
AmazonS3 client = AWSClientFactory.getAnonymousS3Client();
TransferManager manager = new TransferManager(client);
try {
JavaSdkPlugin.getDefault().getLog()
.log(new Status(Status.INFO, JavaSdkPlugin.PLUGIN_ID,
"Downloading the SDK from S3 to location "
+ destination.getAbsolutePath()));
Download download = manager.download(sdkBucketName, "latest/" + sdkFilenamePrefix + ".zip", destination);
monitor.subTask("Downloading latest SDK from S3");
int worked = 0;
int unitWorkInBytes = (int) (download.getProgress().getTotalBytesToTransfer() / totalUnitsOfWork);
while ( !download.isDone() ) {
if ( download.getProgress().getBytesTransferred() / unitWorkInBytes > worked ) {
int newWork = (int) (download.getProgress().getBytesTransferred() / unitWorkInBytes) - worked;
monitor.worked(newWork);
worked += newWork;
}
}
if ( worked < totalUnitsOfWork )
monitor.worked(totalUnitsOfWork - worked);
AmazonClientException ace;
try {
if ( (ace = download.waitForException()) != null) {
throw ace;
}
} catch (InterruptedException e) {
throw new IllegalStateException(
"The SDK download should have already been completed " +
"when waitForException was called, and the method should always " +
"directly return.",
e);
}
if ( !destination.exists() ) {
throw new IllegalStateException(
destination.getAbsolutePath() + " does not exist " +
"after the SDK download completes.");
}
} finally {
// Leave the shared anonymous s3 client open
manager.shutdownNow(false);
}
}
private void unzipSDK(File zipFile, File unzipDestination,
IProgressMonitor monitor, int totalUnitsOfWork) throws IOException {
ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFile));
monitor.subTask("Extracting SDK to workspace metadata directory");
int worked = 0;
long totalSize = zipFile.length();
long totalUnzipped = 0;
int unitWorkInBytes = (int) (totalSize / (double)totalUnitsOfWork);
ZipEntry zipEntry = null;
try {
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
IPath path = new Path(zipEntry.getName());
File destinationFile = new File(unzipDestination, path.toOSString());
if ( zipEntry.isDirectory() ) {
destinationFile.mkdirs();
} else {
long compressedSize = zipEntry.getCompressedSize();
FileOutputStream outputStream = new FileOutputStream(destinationFile);
try {
IOUtils.copy(zipInputStream, outputStream);
} catch (EOFException eof) {
/*
* There is a bug in ZipInputStream, where it might
* incorrectly throw EOFException if the read exceeds
* the current zip-entry size.
*
* http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6519463
*/
JavaSdkPlugin.getDefault().getLog()
.log(new Status(
Status.WARNING, JavaSdkPlugin.PLUGIN_ID,
"Ignore EOFException when unpacking zip-entry " +
zipEntry.getName(),
eof));
}
outputStream.close();
totalUnzipped += compressedSize;
if (totalUnzipped / unitWorkInBytes > worked) {
int newWork = (int) (totalUnzipped / (double)unitWorkInBytes) - worked;
monitor.worked(newWork);
worked += newWork;
}
}
}
} finally {
zipInputStream.close();
}
}
}