/*
* Copyright 2012 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.explorer.cloudformation.wizard;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.core.databinding.AggregateValidationStatus;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.ValidationStatusProvider;
import org.eclipse.core.databinding.beans.PojoObservables;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.fieldassist.FieldDecoration;
import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.regions.Region;
import com.amazonaws.eclipse.core.regions.RegionUtils;
import com.amazonaws.eclipse.core.ui.CancelableThread;
import com.amazonaws.eclipse.core.ui.WebLinkListener;
import com.amazonaws.eclipse.databinding.ChainValidator;
import com.amazonaws.eclipse.databinding.DecorationChangeListener;
import com.amazonaws.eclipse.databinding.NotEmptyValidator;
import com.amazonaws.eclipse.explorer.cloudformation.wizard.CreateStackWizardDataModel.Mode;
import com.amazonaws.eclipse.explorer.sns.CreateTopicDialog;
import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.model.DescribeStacksRequest;
import com.amazonaws.services.cloudformation.model.DescribeStacksResult;
import com.amazonaws.services.cloudformation.model.ListStacksResult;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.Stack;
import com.amazonaws.services.cloudformation.model.StackSummary;
import com.amazonaws.services.cloudformation.model.TemplateParameter;
import com.amazonaws.services.cloudformation.model.ValidateTemplateRequest;
import com.amazonaws.services.cloudformation.model.ValidateTemplateResult;
import com.amazonaws.services.sns.AmazonSNS;
import com.amazonaws.services.sns.model.CreateTopicRequest;
import com.amazonaws.services.sns.model.ListTopicsResult;
import com.amazonaws.services.sns.model.Topic;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* The first page of the stack creation wizard, which prompts for a name and
* template.
*/
class CreateStackWizardFirstPage extends WizardPage {
private static final String LOADING_STACKS = "Loading stacks...";
private static final String OK_MESSAGE = "Provide a name and a template for your new stack.";
private static final String ESTIMATE_COST_OK_MESSAGE = "Provide a template to esitmate the cost";
private static final String VALIDATING = "validating";
private static final String INVALID = "invalid";
private static final String VALID = "valid";
/*
* Data model
*/
private IObservableValue stackName;
private IObservableValue templateUrl;
private IObservableValue templateFile;
private IObservableValue useTemplateFile;
private IObservableValue useTemplateUrl;
private IObservableValue snsTopicArn;
private IObservableValue notifyWithSNS;
private IObservableValue timeoutMinutes;
private IObservableValue rollbackOnFailure;
private IObservableValue templateValidated = new WritableValue();
private final DataBindingContext bindingContext = new DataBindingContext();
private boolean complete = false;
private ValidateTemplateThread validateTemplateThread;
private LoadStackNamesThread loadStackNamesThread;
private Exception templateValidationException;
private Text fileTemplateText;
private Text templateURLText;
private CreateStackWizard wizard;
protected CreateStackWizardFirstPage(CreateStackWizard createStackWizard) {
super("");
wizard = createStackWizard;
if (wizard.getDataModel().getMode() == Mode.EstimateCost) {
setMessage(ESTIMATE_COST_OK_MESSAGE);
} else {
setMessage(OK_MESSAGE);
}
stackName = PojoObservables.observeValue(wizard.getDataModel(), "stackName");
templateUrl = PojoObservables.observeValue(wizard.getDataModel(), "templateUrl");
templateFile = PojoObservables.observeValue(wizard.getDataModel(), "templateFile");
useTemplateFile = PojoObservables.observeValue(wizard.getDataModel(), "useTemplateFile");
useTemplateUrl = PojoObservables.observeValue(wizard.getDataModel(), "useTemplateUrl");
notifyWithSNS = PojoObservables.observeValue(wizard.getDataModel(), "notifyWithSNS");
snsTopicArn = PojoObservables.observeValue(wizard.getDataModel(), "snsTopicArn");
timeoutMinutes = PojoObservables.observeValue(wizard.getDataModel(), "timeoutMinutes");
rollbackOnFailure = PojoObservables.observeValue(wizard.getDataModel(), "rollbackOnFailure");
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt
* .widgets.Composite)
*/
public void createControl(Composite parent) {
final Composite comp = new Composite(parent, SWT.NONE);
GridDataFactory.fillDefaults().grab(true, true).applyTo(comp);
GridLayoutFactory.fillDefaults().numColumns(3).applyTo(comp);
// Unfortunately, we have to manually adjust for field decorations
FieldDecoration fieldDecoration = FieldDecorationRegistry.getDefault().getFieldDecoration(
FieldDecorationRegistry.DEC_ERROR);
int fieldDecorationWidth = fieldDecoration.getImage().getBounds().width;
if (wizard.getDataModel().getMode() != Mode.EstimateCost) {
createStackNameControl(comp, fieldDecorationWidth);
}
createTemplateSelectionControl(comp, fieldDecorationWidth);
// Some fields are only for creation, not update
if ( wizard.getDataModel().getMode() == Mode.Create ) {
createSNSTopicControl(comp, fieldDecorationWidth);
createTimeoutControl(comp);
createRollbackControl(comp);
}
setUpValidation(comp);
// Set initial values for radio buttons
useTemplateFile.setValue(true);
templateValidated.setValue(null);
// If we already have a file template filled in, validate it
if ( wizard.getDataModel().isUsePreselectedTemplateFile() ) {
validateTemplateFile((String) templateFile.getValue());
}
setControl(comp);
}
private void createStackNameControl(final Composite comp, int fieldDecorationWidth) {
// Whether the user already set the stack name.
boolean stackNameExists = false;
new Label(comp, SWT.READ_ONLY).setText("Stack Name: ");
Control stackNameControl = null;
if ( wizard.getDataModel().getMode() == Mode.Create ) {
Text stackNameText = new Text(comp, SWT.BORDER);
bindingContext.bindValue(SWTObservables.observeText(stackNameText, SWT.Modify), stackName)
.updateTargetToModel();
stackNameControl = stackNameText;
} else {
Combo combo = new Combo(comp, SWT.READ_ONLY | SWT.DROP_DOWN);
if (stackName.getValue() != null) {
combo.setItems(new String[] { (String)stackName.getValue() });
stackNameExists = true;
} else {
combo.setItems(new String[] { LOADING_STACKS });
}
combo.select(0);
bindingContext.bindValue(SWTObservables.observeSelection(combo), stackName).updateTargetToModel();
stackNameControl = combo;
stackName.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent event) {
if ( (Boolean) useTemplateFile.getValue() ) {
validateTemplateFile((String) templateFile.getValue());
} else {
validateTemplateUrl((String) templateUrl.getValue());
}
}
});
if (!stackNameExists) {
loadStackNames(combo);
}
}
GridDataFactory.fillDefaults().grab(true, false).span(2, 1).indent(fieldDecorationWidth, 0)
.applyTo(stackNameControl);
ChainValidator<String> stackNameValidationStatusProvider = new ChainValidator<String>(stackName,
new NotEmptyValidator("Please provide a stack name"));
bindingContext.addValidationStatusProvider(stackNameValidationStatusProvider);
addStatusDecorator(stackNameControl, stackNameValidationStatusProvider);
}
/**
* Loads the names of all the stacks asynchronously.
*/
private void loadStackNames(Combo combo) {
CancelableThread.cancelThread(loadStackNamesThread);
templateValidated.setValue(VALIDATING);
loadStackNamesThread = new LoadStackNamesThread(combo);
loadStackNamesThread.start();
}
private final class LoadStackNamesThread extends CancelableThread {
private Combo combo;
private LoadStackNamesThread(Combo combo) {
super();
this.combo = combo;
}
public void run() {
AmazonCloudFormation cf = getCloudFormationClient();
ListStacksResult listStacks = cf.listStacks();
final List<String> stackNames = new ArrayList<String>();
for ( StackSummary summary : listStacks.getStackSummaries() ) {
stackNames.add(summary.getStackName());
}
Display.getDefault().syncExec(new Runnable() {
public void run() {
try {
synchronized ( this ) {
if (!isCanceled()) {
combo.setItems(stackNames.toArray(new String[stackNames.size()]));
combo.select(0);
templateValidated.setValue(VALID);
}
}
} finally {
setRunning(false);
}
}
});
}
}
private void createTemplateSelectionControl(final Composite comp, int fieldDecorationWidth) {
Group stackTemplateSourceGroup = new Group(comp, SWT.NONE);
GridDataFactory.fillDefaults().grab(true, false).span(3, 1).indent(fieldDecorationWidth, 0)
.applyTo(stackTemplateSourceGroup);
GridLayoutFactory.fillDefaults().numColumns(3).applyTo(stackTemplateSourceGroup);
stackTemplateSourceGroup.setText("Stack Template Source:");
createTemplateFileControl(stackTemplateSourceGroup, fieldDecorationWidth);
createTemplateUrlControl(stackTemplateSourceGroup, fieldDecorationWidth);
}
private void createTemplateUrlControl(Group stackTemplateSourceGroup, int fieldDecorationWidth) {
final Button templateUrlOption = new Button(stackTemplateSourceGroup, SWT.RADIO);
templateUrlOption.setText("Template URL: ");
templateURLText = new Text(stackTemplateSourceGroup, SWT.BORDER);
templateURLText.setEnabled(false);
GridDataFactory.fillDefaults().grab(true, false).indent(fieldDecorationWidth, 0).applyTo(templateURLText);
Link link = new Link(stackTemplateSourceGroup, SWT.None);
// TODO: this should really live in the regions file, not hardcoded here
Region currentRegion = RegionUtils.getCurrentRegion();
final String sampleUrl = String.format(
"http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-sample-templates-%s.html",
currentRegion.getId());
link.setText("<a href=\"" +
sampleUrl +
"\">Browse for samples</a>");
link.addListener(SWT.Selection, new WebLinkListener());
bindingContext.bindValue(SWTObservables.observeText(templateURLText, SWT.Modify), templateUrl)
.updateTargetToModel();
bindingContext.bindValue(SWTObservables.observeSelection(templateUrlOption), useTemplateUrl)
.updateTargetToModel();
ChainValidator<String> templateUrlValidationStatusProvider = new ChainValidator<String>(templateUrl,
useTemplateUrl, new NotEmptyValidator("Please provide a valid URL for your template"));
bindingContext.addValidationStatusProvider(templateUrlValidationStatusProvider);
addStatusDecorator(templateURLText, templateUrlValidationStatusProvider);
templateUrlOption.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
boolean selected = templateUrlOption.getSelection();
templateURLText.setEnabled(selected);
}
});
}
private void createTemplateFileControl(Group stackTemplateSourceGroup, int fieldDecorationWidth) {
Button fileTemplateOption = new Button(stackTemplateSourceGroup, SWT.RADIO);
fileTemplateOption.setText("Template File: ");
fileTemplateOption.setSelection(true);
fileTemplateText = new Text(stackTemplateSourceGroup, SWT.BORDER | SWT.READ_ONLY);
GridDataFactory.fillDefaults().grab(true, false).indent(fieldDecorationWidth, 0).applyTo(fileTemplateText);
Button browseButton = new Button(stackTemplateSourceGroup, SWT.PUSH);
browseButton.setText("Browse...");
Listener fileTemplateSelectionListener = new Listener() {
public void handleEvent(Event event) {
if ( (Boolean) useTemplateFile.getValue() ) {
FileDialog dialog = new FileDialog(getShell(), SWT.OPEN);
String result = dialog.open();
if ( result != null ) {
fileTemplateText.setText(result);
}
}
}
};
browseButton.addListener(SWT.Selection, fileTemplateSelectionListener);
fileTemplateText.addListener(SWT.MouseUp, fileTemplateSelectionListener);
bindingContext.bindValue(SWTObservables.observeSelection(fileTemplateOption), useTemplateFile)
.updateTargetToModel();
bindingContext.bindValue(SWTObservables.observeText(fileTemplateText, SWT.Modify), templateFile)
.updateTargetToModel();
ChainValidator<String> templateFileValidationStatusProvider = new ChainValidator<String>(templateFile,
useTemplateFile, new NotEmptyValidator("Please provide a valid file for your template"));
bindingContext.addValidationStatusProvider(templateFileValidationStatusProvider);
addStatusDecorator(fileTemplateText, templateFileValidationStatusProvider);
}
private void createSNSTopicControl(final Composite comp, int fieldDecorationWidth) {
final Button notifyWithSNSButton = new Button(comp, SWT.CHECK);
notifyWithSNSButton.setText("SNS Topic (Optional):");
final Combo snsTopicCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
GridDataFactory.fillDefaults().grab(true, false).indent(fieldDecorationWidth, 0).applyTo(snsTopicCombo);
loadTopics(snsTopicCombo);
bindingContext.bindValue(SWTObservables.observeSelection(notifyWithSNSButton), notifyWithSNS)
.updateTargetToModel();
bindingContext.bindValue(SWTObservables.observeSelection(snsTopicCombo), snsTopicArn).updateTargetToModel();
ChainValidator<String> snsTopicValidationStatusProvider = new ChainValidator<String>(snsTopicArn,
notifyWithSNS, new NotEmptyValidator("Please select an SNS notification topic"));
bindingContext.addValidationStatusProvider(snsTopicValidationStatusProvider);
addStatusDecorator(snsTopicCombo, snsTopicValidationStatusProvider);
final Button newTopicButton = new Button(comp, SWT.PUSH);
newTopicButton.setText("Create New Topic");
newTopicButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
CreateTopicDialog dialog = new CreateTopicDialog();
if ( dialog.open() == 0 ) {
try {
AwsToolkitCore.getClientFactory().getSNSClient()
.createTopic(new CreateTopicRequest().withName(dialog.getTopicName()));
} catch ( Exception ex ) {
AwsToolkitCore.getDefault().logException("Failed to create new topic", ex);
}
loadTopics(snsTopicCombo);
}
}
});
snsTopicCombo.setEnabled(false);
newTopicButton.setEnabled(false);
notifyWithSNSButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
boolean selection = notifyWithSNSButton.getSelection();
snsTopicCombo.setEnabled(selection);
newTopicButton.setEnabled(selection);
}
});
}
private void createTimeoutControl(final Composite comp) {
new Label(comp, SWT.None).setText("Creation Timeout:");
Combo timeoutCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
GridDataFactory.fillDefaults().grab(false, false).span(2, 1).applyTo(timeoutCombo);
timeoutCombo.setItems(new String[] { "None", "5 minutes", "10 minutes", "15 minutes", "20 minutes",
"30 minutes", "60 minutes", "90 minutes", });
timeoutCombo.select(0);
UpdateValueStrategy timeoutUpdateStrategy = new UpdateValueStrategy(UpdateValueStrategy.POLICY_UPDATE);
timeoutUpdateStrategy.setConverter(new IConverter() {
public Object getToType() {
return Integer.class;
}
public Object getFromType() {
return String.class;
}
public Object convert(Object fromObject) {
String value = (String) fromObject;
if ( "None".equals(value) ) {
return 0;
} else {
String minutes = value.substring(0, value.indexOf(' '));
return Integer.parseInt(minutes);
}
}
});
bindingContext.bindValue(SWTObservables.observeSelection(timeoutCombo), timeoutMinutes, timeoutUpdateStrategy,
new UpdateValueStrategy(UpdateValueStrategy.POLICY_NEVER)).updateTargetToModel();
}
private void createRollbackControl(final Composite comp) {
Button rollbackButton = new Button(comp, SWT.CHECK);
rollbackButton.setText("Rollback on Failure");
GridDataFactory.fillDefaults().grab(false, false).span(3, 1).applyTo(rollbackButton);
rollbackButton.setSelection(true);
bindingContext.bindValue(SWTObservables.observeSelection(rollbackButton), rollbackOnFailure)
.updateTargetToModel();
}
private void setUpValidation(final Composite comp) {
// Change listeners to re-validate the template whenever
// the customer changes whether to use a file or a URL
templateUrl.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent event) {
if ( ((String) templateUrl.getValue()).length() > 0 ) {
validateTemplateUrl((String) templateUrl.getValue());
}
}
});
useTemplateUrl.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent event) {
if ( (Boolean) useTemplateUrl.getValue() && ((String) templateUrl.getValue()).length() > 0 ) {
validateTemplateUrl((String) templateUrl.getValue());
}
}
});
templateFile.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent event) {
validateTemplateFile((String) templateFile.getValue());
}
});
useTemplateFile.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent event) {
if ( (Boolean) useTemplateFile.getValue() ) {
validateTemplateFile((String) templateFile.getValue());
}
}
});
// Status validator for template validation, which occurs out of band
IValidator templateValidator = new IValidator() {
public IStatus validate(Object value) {
if ( value == null ) {
return ValidationStatus.error("No template selected");
}
if ( ((String) value).equals(VALID) ) {
return ValidationStatus.ok();
} else if ( ((String) value).equals(VALIDATING) ) {
return ValidationStatus.warning("Validating template...");
} else if ( ((String) value).equals(INVALID) ) {
if ( templateValidationException != null ) {
return ValidationStatus.error("Invalid template: " + templateValidationException.getMessage());
} else {
return ValidationStatus.error("No template selected");
}
}
return ValidationStatus.ok();
}
};
bindingContext.addValidationStatusProvider(new ChainValidator<String>(templateValidated, templateValidator));
// Also hook up this template validator to the two template fields
// conditionally
addStatusDecorator(fileTemplateText, new ChainValidator<String>(templateValidated, useTemplateFile,
templateValidator));
addStatusDecorator(templateURLText, new ChainValidator<String>(templateValidated, useTemplateUrl,
templateValidator));
// Finally provide aggregate status reporting for the entire wizard page
final AggregateValidationStatus aggregateValidationStatus = new AggregateValidationStatus(bindingContext,
AggregateValidationStatus.MAX_SEVERITY);
aggregateValidationStatus.addChangeListener(new IChangeListener() {
public void handleChange(ChangeEvent event) {
Object value = aggregateValidationStatus.getValue();
if ( value instanceof IStatus == false )
return;
IStatus status = (IStatus) value;
if ( status.isOK() ) {
setErrorMessage(null);
if (wizard.getDataModel().getMode() == Mode.EstimateCost) {
setMessage(ESTIMATE_COST_OK_MESSAGE, Status.OK);
} else {
setMessage(OK_MESSAGE, Status.OK);
}
} else if (status.getSeverity() == Status.WARNING) {
setErrorMessage(null);
setMessage(status.getMessage(), Status.WARNING);
} else if (status.getSeverity() == Status.ERROR) {
setErrorMessage(status.getMessage());
}
setComplete(status.isOK());
}
});
}
/**
* Adds a control status decorator for the control given.
*/
private void addStatusDecorator(final Control control, ValidationStatusProvider validationStatusProvider) {
ControlDecoration decoration = new ControlDecoration(control, SWT.TOP | SWT.LEFT);
decoration.setDescriptionText("Invalid value");
FieldDecoration fieldDecoration = FieldDecorationRegistry.getDefault().getFieldDecoration(
FieldDecorationRegistry.DEC_ERROR);
decoration.setImage(fieldDecoration.getImage());
new DecorationChangeListener(decoration, validationStatusProvider.getValidationStatus());
}
@Override
public boolean isPageComplete() {
return complete;
}
private void setComplete(boolean complete) {
this.complete = complete;
if ( getWizard().getContainer() != null && getWizard().getContainer().getCurrentPage() != null )
getWizard().getContainer().updateButtons();
}
/**
* Loads all SNS topics into the dropdown given
*/
private void loadTopics(final Combo snsTopicCombo) {
snsTopicCombo.setItems(new String[] { "Loading..." });
new Thread() {
@Override
public void run() {
AmazonSNS sns = AwsToolkitCore.getClientFactory().getSNSClient();
ListTopicsResult topicsResult = sns.listTopics();
final List<String> arns = new ArrayList<String>();
for ( Topic topic : topicsResult.getTopics() ) {
arns.add(topic.getTopicArn());
}
Display.getDefault().syncExec(new Runnable() {
public void run() {
if ( !snsTopicCombo.isDisposed() ) {
snsTopicCombo.setItems(arns.toArray(new String[arns.size()]));
}
}
});
}
}.start();
}
/**
* Validates the template file given in a separate thread.
*/
private void validateTemplateFile(String filePath) {
CancelableThread.cancelThread(validateTemplateThread);
templateValidated.setValue(VALIDATING);
try {
String fileContents = FileUtils.readFileToString(new File(filePath), "UTF8");
validateTemplateThread = new ValidateTemplateThread(
new ValidateTemplateRequest().withTemplateBody(fileContents));
validateTemplateThread.start();
} catch ( Exception e ) {
templateValidated.setValue(INVALID);
templateValidationException = e;
}
}
/**
* Validates the template url given in a separate thread.
*/
private void validateTemplateUrl(String url) {
CancelableThread.cancelThread(validateTemplateThread);
templateValidated.setValue(VALIDATING);
validateTemplateThread = new ValidateTemplateThread(new ValidateTemplateRequest().withTemplateURL(url));
validateTemplateThread.start();
}
/**
* Cancelable thread to validate a template and update the validation
* status.
*/
private final class ValidateTemplateThread extends CancelableThread {
private final ValidateTemplateRequest rq;
private ValidateTemplateThread(ValidateTemplateRequest rq) {
this.rq = rq;
}
@Override
public void run() {
ValidateTemplateResult validateTemplateResult;
Stack existingStack = null;
Map templateMap;
try {
// TODO: region should come from context for file-based actions
AmazonCloudFormation cf = getCloudFormationClient();
validateTemplateResult = cf.validateTemplate(rq);
if ( wizard.getDataModel().getMode() == Mode.Update && wizard.getDataModel().getStackName() != LOADING_STACKS ) {
DescribeStacksResult describeStacks = cf.describeStacks(new DescribeStacksRequest()
.withStackName(wizard.getDataModel().getStackName()));
if ( describeStacks.getStacks().size() == 1 ) {
existingStack = describeStacks.getStacks().iterator().next();
}
}
String templateBody = null;
if ( rq.getTemplateBody() != null ) {
templateBody = rq.getTemplateBody();
} else {
InputStream in = new URL(rq.getTemplateURL()).openStream();
try {
templateBody = IOUtils.toString(in);
} finally {
IOUtils.closeQuietly(in);
}
}
templateMap = parseTemplate(templateBody);
wizard.getDataModel().setTemplateBody(templateBody);
} catch ( Exception e ) {
templateValidationException = e;
Display.getDefault().syncExec(new Runnable() {
public void run() {
synchronized ( this ) {
if ( !isCanceled() ) {
templateValidated.setValue(INVALID);
}
}
}
});
setRunning(false);
return;
}
final List<TemplateParameter> templateParams = validateTemplateResult.getParameters();
final Map templateJson = templateMap;
final List<String> requiredCapabilities = validateTemplateResult.getCapabilities();
final Stack stack = existingStack;
Display.getDefault().syncExec(new Runnable() {
public void run() {
try {
synchronized ( this ) {
if ( !isCanceled() ) {
wizard.getDataModel().setTemplateParameters(templateParams);
wizard.setNeedsSecondPage(!templateParams.isEmpty());
wizard.getDataModel().setTemplate(templateJson);
wizard.getDataModel().setRequiredCapabilities(requiredCapabilities);
if ( stack != null ) {
for ( Parameter param : stack.getParameters() ) {
boolean noEcho = false;
// This is a pain, but any "noEcho" parameters get returned as asterisks in the service response.
// The customer must fill these values out again, even for a running stack.
for ( TemplateParameter templateParam : wizard.getDataModel().getTemplateParameters() ) {
if (templateParam.getNoEcho() && templateParam.getParameterKey().equals(param.getParameterKey())) {
noEcho = true;
break;
}
}
if ( !noEcho ) {
wizard.getDataModel().getParameterValues()
.put(param.getParameterKey(), param.getParameterValue());
}
}
}
templateValidated.setValue(VALID);
}
}
} finally {
setRunning(false);
}
}
});
}
}
/**
* Parses the (already validated) template given and returns a map of its
* structure.
*/
private Map parseTemplate(String templateBody) throws JsonParseException, IOException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(templateBody, Map.class);
}
/**
* @return
*/
private AmazonCloudFormation getCloudFormationClient() {
AmazonCloudFormation cf = AwsToolkitCore.getClientFactory().getCloudFormationClient();
return cf;
}
/**
* Whether we can flip to the next page depends on whether there's one to go to.
*/
@Override
public boolean canFlipToNextPage() {
return wizard.needsSecondPage() && super.canFlipToNextPage();
}
}