/* * Copyright 2010-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.elasticbeanstalk.server.ui; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.eclipse.core.databinding.beans.PojoObservables; import org.eclipse.core.databinding.observable.set.IObservableSet; import org.eclipse.core.databinding.observable.set.WritableSet; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.databinding.swt.ISWTObservableValue; import org.eclipse.jface.databinding.swt.SWTObservables; import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.jface.wizard.WizardDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.events.HyperlinkAdapter; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.widgets.Hyperlink; import org.eclipse.ui.statushandlers.StatusManager; import org.eclipse.wst.server.ui.wizard.IWizardHandle; import org.eclipse.wst.server.ui.wizard.WizardFragment; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; 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.regions.ServiceAbbreviations; import com.amazonaws.eclipse.core.ui.CancelableThread; import com.amazonaws.eclipse.databinding.BooleanValidator; import com.amazonaws.eclipse.databinding.ChainValidator; import com.amazonaws.eclipse.databinding.DecorationChangeListener; import com.amazonaws.eclipse.databinding.NotInListValidator; import com.amazonaws.eclipse.elasticbeanstalk.ConfigurationOptionConstants; import com.amazonaws.eclipse.elasticbeanstalk.ElasticBeanstalkPlugin; import com.amazonaws.eclipse.elasticbeanstalk.deploy.DeployWizardDataModel; import com.amazonaws.eclipse.elasticbeanstalk.server.ui.databinding.ApplicationNameValidator; import com.amazonaws.eclipse.elasticbeanstalk.server.ui.databinding.EnvironmentNameValidator; import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalk; import com.amazonaws.services.elasticbeanstalk.model.ApplicationDescription; import com.amazonaws.services.elasticbeanstalk.model.EnvironmentDescription; import com.amazonaws.services.elasticbeanstalk.model.EnvironmentStatus; final class DeployWizardApplicationSelectionPage extends AbstractDeployWizardPage { private static final String LOADING = "Loading..."; private static final String NONE_FOUND = "None found"; private static final String VPC_CONFIGURATION_DOC_URL = "https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/vpc.html"; private DeployWizardVpcConfigurationPage vpcConfigPage; // Region controls private Combo regionCombo; private SelectionListener regionChangeListener; // Application controls private Button createNewApplicationRadioButton; private ISWTObservableValue createNewApplicationRadioButtonObservable; private Combo existingApplicationCombo; private Text newApplicationDescriptionText; private Text newApplicationNameText; private ControlDecoration newApplicationNameDecoration; private Button existingApplicationRadioButton; // Environment controls private ControlDecoration newEnvironmentNameDecoration; private Text newEnvironmentNameText; private Text newEnvironmentDescriptionText; private Combo environmentTypeCombo; private Button useNonDefaultVpcButton; private boolean useNonDefaultVpc = false; // Asynchronous workers private LoadApplicationsThread loadApplicationsThread; private LoadEnvironmentsThread loadEnvironmentsThread; private IObservableSet existingEnvironmentNames = new WritableSet(); private IObservableSet existingApplicationNames = new WritableSet(); private IObservableValue environmentNamesLoaded = new WritableValue(); private IObservableValue applicationNamesLoaded = new WritableValue(); private ISWTObservableValue newApplicationNameTextObservable; private ISWTObservableValue newApplicationDescriptionTextObservable; private ISWTObservableValue newEnvironmentDescriptionTextObservable; private ISWTObservableValue newEnvironmentNameTextObservable; private ISWTObservableValue environmentTypeComboObservable; private ISWTObservableValue useNonDefaultVpcButtonObservable; // Status of our connectivity to AWS Elastic Beanstalk private IStatus connectionStatus; private AWSElasticBeanstalk elasticBeanstalkClient; DeployWizardApplicationSelectionPage(DeployWizardDataModel wizardDataModel) { super(wizardDataModel); environmentNamesLoaded.setValue(false); applicationNamesLoaded.setValue(false); vpcConfigPage = new DeployWizardVpcConfigurationPage(wizardDataModel); } @Override public List<WizardFragment> getChildFragments() { List<WizardFragment> fragmentList = new ArrayList<WizardFragment>(); if (useNonDefaultVpc) { fragmentList.add(vpcConfigPage); } return fragmentList; } /* (non-Javadoc) * @see org.eclipse.wst.server.ui.wizard.WizardFragment#createComposite(org.eclipse.swt.widgets.Composite, org.eclipse.wst.server.ui.wizard.IWizardHandle) */ @Override public Composite createComposite(Composite parent, IWizardHandle handle) { wizardHandle = handle; elasticBeanstalkClient = AwsToolkitCore.getClientFactory().getElasticBeanstalkClientByEndpoint(wizardDataModel.getRegionEndpoint()); handle.setImageDescriptor(AwsToolkitCore.getDefault().getImageRegistry().getDescriptor(AwsToolkitCore.IMAGE_AWS_LOGO)); handle.setMessage("", IStatus.OK); connectionStatus = testConnection(); if (connectionStatus.isOK()) { Composite composite = new Composite(parent, SWT.NONE); composite.setLayout(new GridLayout(2, false)); createRegionSection(composite); createApplicationSection(composite); createEnvironmentSection(composite); createImportSection(composite); bindControls(); initializeDefaults(); return composite; } else { return new ErrorComposite(parent, SWT.NONE, connectionStatus); } } private IStatus testConnection() { try { wizardHandle.setMessage("", IStatus.OK); wizardHandle.run(true, false, new CheckAccountRunnable(elasticBeanstalkClient)); wizardHandle.setMessage("", IStatus.OK); return Status.OK_STATUS; } catch (InvocationTargetException ite) { String errorMessage = "Unable to connect to AWS Elastic Beanstalk. "; try { throw ite.getCause(); } catch (AmazonServiceException ase) { errorMessage += "Make sure you've registered your AWS account for the AWS Elastic Beanstalk service."; } catch (AmazonClientException ace) { errorMessage += "Make sure your computer is connected to the internet, and any network firewalls or proxys are configured appropriately."; } catch (Throwable t) {} return new Status(IStatus.ERROR, ElasticBeanstalkPlugin.PLUGIN_ID, errorMessage, ite.getCause()); } catch (InterruptedException e) { return Status.CANCEL_STATUS; } } /** * Initializes the page to its default selections */ private void initializeDefaults() { createNewApplicationRadioButtonObservable.setValue(true); existingApplicationRadioButton.setSelection(false); newApplicationNameTextObservable.setValue(""); newApplicationDescriptionTextObservable.setValue(""); newEnvironmentNameTextObservable.setValue(""); newEnvironmentDescriptionTextObservable.setValue(""); environmentTypeComboObservable.setValue(ConfigurationOptionConstants.LOAD_BALANCED_ENV); if (RegionUtils.isServiceSupportedInCurrentRegion(ServiceAbbreviations.BEANSTALK)) { regionCombo.setText(RegionUtils.getCurrentRegion().getName()); wizardDataModel.setRegion(RegionUtils.getCurrentRegion()); } else { regionCombo.setText(RegionUtils.getRegion(ElasticBeanstalkPlugin.DEFAULT_REGION).getName()); wizardDataModel.setRegion(RegionUtils.getRegion(ElasticBeanstalkPlugin.DEFAULT_REGION)); } regionChangeListener.widgetSelected(null); // Trigger the standard enabled / disabled control logic radioButtonSelected(createNewApplicationRadioButton); } private void createRegionSection(Composite composite) { Group regionGroup = newGroup(composite, "", 2); regionGroup.setLayout(new GridLayout(2, false)); newLabel(regionGroup, "Region:"); regionCombo = newCombo(regionGroup); for (Region region : RegionUtils.getRegionsForService(ServiceAbbreviations.BEANSTALK) ) { regionCombo.add(region.getName()); regionCombo.setData(region.getName(), region); } Region region = RegionUtils.getRegionByEndpoint(wizardDataModel.getRegionEndpoint()); regionCombo.setText(region.getName()); newFillingLabel(regionGroup, "AWS regions are geographically isolated, " + "allowing you to position your Elastic Beanstalk application closer to you or your customers.", 2); regionChangeListener = new SelectionListener() { public void widgetSelected(SelectionEvent e) { Region region = (Region)regionCombo.getData(regionCombo.getText()); String endpoint = region.getServiceEndpoints().get(ServiceAbbreviations.BEANSTALK); elasticBeanstalkClient = AwsToolkitCore.getClientFactory().getElasticBeanstalkClientByEndpoint(endpoint); wizardDataModel.setRegion(region); if (wizardDataModel.getKeyPairComposite() != null) { wizardDataModel.getKeyPairComposite().getKeyPairSelectionTable().setEc2RegionOverride(region); } createNewApplicationRadioButtonObservable.setValue(true); existingApplicationCombo.setEnabled(false); newApplicationNameText.setEnabled(true); newApplicationDescriptionText.setEnabled(true); refreshApplications(); refreshEnvironments(); } public void widgetDefaultSelected(SelectionEvent e) { widgetSelected(e); } }; regionCombo.addSelectionListener(regionChangeListener); } private void createApplicationSection(Composite composite) { Group applicationGroup = newGroup(composite, "Application:", 2); applicationGroup.setLayout(new GridLayout(2, false)); createNewApplicationRadioButton = newRadioButton(applicationGroup, "Create a new application:", 2, true); createNewApplicationRadioButtonObservable = SWTObservables.observeSelection( createNewApplicationRadioButton); new NewApplicationOptionsComposite(applicationGroup); existingApplicationRadioButton = newRadioButton(applicationGroup, "Choose an existing application:", 1); existingApplicationCombo = newCombo(applicationGroup); existingApplicationCombo.setEnabled(false); } private void createEnvironmentSection(Composite composite) { Group environmentOptionsGroup = newGroup(composite, "Environment:", 2); environmentOptionsGroup.setLayout(new GridLayout(2, false)); new NewEnvironmentOptionsComposite(environmentOptionsGroup); } private void createImportSection(final Composite composite) { Hyperlink link = new Hyperlink(composite, SWT.None); link.addHyperlinkListener(new HyperlinkAdapter() { @Override public void linkActivated(HyperlinkEvent e) { ImportEnvironmentsWizard newWizard = new ImportEnvironmentsWizard(); WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newWizard); dialog.open(); } }); link.setText("Import an existing environment into the Servers view"); link.setUnderlined(true); GridData layoutData = new GridData(); layoutData.horizontalSpan = 2; link.setLayoutData(layoutData); } private void bindControls() { super.initializeValidators(); newApplicationNameTextObservable = SWTObservables.observeText(newApplicationNameText, SWT.Modify); bindingContext.bindValue( newApplicationNameTextObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.NEW_APPLICATION_NAME), null, null); ChainValidator<String> applicationNameValidator = new ChainValidator<String>( newApplicationNameTextObservable, createNewApplicationRadioButtonObservable, new ApplicationNameValidator(), new NotInListValidator<String>(existingApplicationNames, "Duplicate application name.")); bindingContext.addValidationStatusProvider(applicationNameValidator); bindingContext.addValidationStatusProvider(new ChainValidator<Boolean>(applicationNamesLoaded, new BooleanValidator("Appliction names not yet loaded"))); new DecorationChangeListener(newApplicationNameDecoration, applicationNameValidator.getValidationStatus()); newApplicationDescriptionTextObservable = SWTObservables.observeText(newApplicationDescriptionText, SWT.Modify); bindingContext.bindValue( newApplicationDescriptionTextObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.NEW_APPLICATION_DESCRIPTION), null, null); bindingContext.bindValue( createNewApplicationRadioButtonObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.CREATING_NEW_APPLICATION), null, null); // Existing application bindings bindingContext.bindValue(SWTObservables.observeSelection(existingApplicationCombo), PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.EXISTING_APPLICATION_NAME)); // New environment bindings newEnvironmentNameTextObservable = SWTObservables.observeText(newEnvironmentNameText, SWT.Modify); bindingContext.bindValue( newEnvironmentNameTextObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.NEW_ENVIRONMENT_NAME), null, null); newEnvironmentDescriptionTextObservable = SWTObservables.observeText(newEnvironmentDescriptionText, SWT.Modify); bindingContext.bindValue( newEnvironmentDescriptionTextObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.NEW_ENVIRONMENT_DESCRIPTION), null, null); ChainValidator<String> environmentNameValidator = new ChainValidator<String>( newEnvironmentNameTextObservable, new EnvironmentNameValidator(), new NotInListValidator<String>(existingEnvironmentNames, "Duplicate environment name.")); bindingContext.addValidationStatusProvider(environmentNameValidator); bindingContext.addValidationStatusProvider(new ChainValidator<Boolean>(environmentNamesLoaded, new BooleanValidator("Environment names not yet loaded"))); new DecorationChangeListener(newEnvironmentNameDecoration, environmentNameValidator.getValidationStatus()); environmentTypeComboObservable = SWTObservables.observeSelection(environmentTypeCombo); bindingContext.bindValue( environmentTypeComboObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.ENVIRONMENT_TYPE)); useNonDefaultVpcButtonObservable = SWTObservables.observeSelection(useNonDefaultVpcButton); bindingContext.bindValue( useNonDefaultVpcButtonObservable, PojoObservables.observeValue(wizardDataModel, DeployWizardDataModel.USE_NON_DEFAULT_VPC)); } /** * Asynchronously updates the set of existing applications. Must be called * from the UI thread. */ private void refreshApplications() { /* * While the values load, we need to disable the controls and fake a * radio button selected event. */ createNewApplicationRadioButton.setSelection(true); existingApplicationRadioButton.setSelection(false); existingApplicationRadioButton.setEnabled(false); radioButtonSelected(createNewApplicationRadioButton); existingApplicationCombo.setItems(new String[] { LOADING }); existingApplicationCombo.select(0); cancelThread(loadApplicationsThread); applicationNamesLoaded.setValue(false); loadApplicationsThread = new LoadApplicationsThread(); loadApplicationsThread.start(); } /** * Safely cancels the thread given. */ private void cancelThread(CancelableThread thread) { if ( thread != null ) { synchronized (thread) { if ( thread.isRunning() ) { thread.cancel(); } } } } /** * Asynchronously updates the set of existing environments for the current * application. */ private void refreshEnvironments() { cancelThread(loadEnvironmentsThread); environmentNamesLoaded.setValue(false); loadEnvironmentsThread = new LoadEnvironmentsThread(); loadEnvironmentsThread.start(); } /** * We handle radio button selections by enabling and disabling various * controls. There are only two sources of these events that we care about. */ @Override protected void radioButtonSelected(Object source) { if ( source == existingApplicationRadioButton || source == createNewApplicationRadioButton) { boolean isCreatingNewApplication = (Boolean) createNewApplicationRadioButtonObservable.getValue(); existingApplicationCombo.setEnabled(!isCreatingNewApplication); newApplicationNameText.setEnabled(isCreatingNewApplication); newApplicationDescriptionText.setEnabled(isCreatingNewApplication); } } @Override public void enter() { super.enter(); if ( connectionStatus != null && connectionStatus.isOK() ) { refreshApplications(); refreshEnvironments(); } } /** * Cancel any outstanding work before exiting. */ @Override public void exit() { cancelThread(loadApplicationsThread); cancelThread(loadEnvironmentsThread); super.exit(); } private boolean isServiceSignUpException(Exception e) { if (e instanceof AmazonServiceException) { AmazonServiceException ase = (AmazonServiceException)e; return "OptInRequired".equalsIgnoreCase(ase.getErrorCode()); } return false; } private IStatus newServiceSignUpErrorStatus(Exception e) { return new Status(Status.ERROR, ElasticBeanstalkPlugin.PLUGIN_ID, "Error connecting to AWS Elastic Beanstalk. " + "Make sure you've signed up your AWS account for Elastic Beanstalk, and " + "waited for the changes to propagate.", e); } private class NewApplicationOptionsComposite extends Composite { public NewApplicationOptionsComposite(Composite parent) { super(parent, SWT.NONE); setLayout(new GridLayout(2, false)); GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); gridData.horizontalIndent = 15; gridData.horizontalSpan = 2; setLayoutData(gridData); newLabel(this, "Name:"); newApplicationNameText = newText(this); newApplicationNameDecoration = newControlDecoration( newApplicationNameText, "Enter a new application name or select an existing application."); newLabel(this, "Description:"); newApplicationDescriptionText = newText(this); } } private class NewEnvironmentOptionsComposite extends Composite { public NewEnvironmentOptionsComposite(Composite parent) { super(parent, SWT.NONE); setLayout(new GridLayout(2, false)); GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false); gridData.horizontalIndent = 15; gridData.horizontalSpan = 2; setLayoutData(gridData); newLabel(this, "Name:"); newEnvironmentNameText = newText(this); newEnvironmentNameDecoration = newControlDecoration( newEnvironmentNameText, "Enter a new environment name"); newLabel(this, "Description:"); newEnvironmentDescriptionText = newText(this); final String[] items = { ConfigurationOptionConstants.SINGLE_INSTANCE_ENV, ConfigurationOptionConstants.LOAD_BALANCED_ENV, ConfigurationOptionConstants.WORKER_ENV }; newLabel(this, "Type:"); environmentTypeCombo = newCombo(this); environmentTypeCombo.setItems(items); useNonDefaultVpcButton = newCheckbox(parent, "", 1); useNonDefaultVpcButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { useNonDefaultVpc = useNonDefaultVpcButton.getSelection(); wizardHandle.update(); } }); createVpcSelectionLabel(parent); } } private void createVpcSelectionLabel(Composite composite) { adjustLinkLayout(newLink(composite, "Select the VPC to use when creating your environment. " + "<a href=\"" + VPC_CONFIGURATION_DOC_URL + "\">Learn more</a>"), 1); } private final class LoadApplicationsThread extends CancelableThread { @Override public void run() { final List<ApplicationDescription> applications = new ArrayList<ApplicationDescription>(); try { applications.addAll(elasticBeanstalkClient.describeApplications().getApplications()); } catch (Exception e) { if (isServiceSignUpException(e)) { StatusManager.getManager().handle(newServiceSignUpErrorStatus(e), StatusManager.SHOW | StatusManager.LOG); } else { Status status = new Status(Status.ERROR, ElasticBeanstalkPlugin.PLUGIN_ID, "Unable to load existing applications: " + e.getMessage(), e); StatusManager.getManager().handle(status, StatusManager.LOG); } setRunning(false); return; } Display.getDefault().asyncExec(new Runnable() { public void run() { try { List<String> applicationNames = new ArrayList<String>(); for ( ApplicationDescription application : applications ) { applicationNames.add(application.getApplicationName()); } Collections.sort(applicationNames); synchronized (LoadApplicationsThread.this) { if ( !isCanceled() ) { existingApplicationNames.clear(); existingApplicationNames.addAll(applicationNames); existingApplicationCombo.removeAll(); for ( String applicationName : applicationNames ) { existingApplicationCombo.add(applicationName); } if ( applications.size() > 0 ) { existingApplicationCombo.select(0); existingApplicationRadioButton.setEnabled(true); } else { existingApplicationCombo.setEnabled(false); existingApplicationRadioButton.setEnabled(false); createNewApplicationRadioButtonObservable.setValue(true); existingApplicationCombo.setItems(new String[] { NONE_FOUND}); existingApplicationCombo.select(0); } applicationNamesLoaded.setValue(true); runValidators(); } } } finally { setRunning(false); } } }); } } private final class LoadEnvironmentsThread extends CancelableThread { @Override public void run() { final List<EnvironmentDescription> environments = new ArrayList<EnvironmentDescription>(); try { environments.addAll(elasticBeanstalkClient.describeEnvironments().getEnvironments()); } catch (Exception e) { if (isServiceSignUpException(e)) { StatusManager.getManager().handle(newServiceSignUpErrorStatus(e), StatusManager.SHOW | StatusManager.LOG); } else { Status status = new Status(Status.ERROR, ElasticBeanstalkPlugin.PLUGIN_ID, "Unable to load existing environments: " + e.getMessage(), e); StatusManager.getManager().handle(status, StatusManager.LOG); } setRunning(false); return; } Display.getDefault().asyncExec(new Runnable() { public void run() { try { List<String> environmentNames = new ArrayList<String>(); for ( EnvironmentDescription environment : environments ) { // Skip any terminated environments, since we can safely reuse their names if ( isEnvironmentTerminated(environment) ) { continue; } environmentNames.add(environment.getEnvironmentName()); } Collections.sort(environmentNames); synchronized (LoadEnvironmentsThread.this) { if ( !isCanceled() ) { existingEnvironmentNames.clear(); existingEnvironmentNames.addAll(environmentNames); environmentNamesLoaded.setValue(true); runValidators(); } } } finally { setRunning(false); } } }); } } private boolean isEnvironmentTerminated(EnvironmentDescription environment) { if (environment == null || environment.getStatus() == null) { return false; } try { EnvironmentStatus status = EnvironmentStatus.valueOf(environment.getStatus()); return (status == EnvironmentStatus.Terminated); } catch (Exception e) { return false; } } @Override public String getPageTitle() { return "Configure Application and Environment"; } @Override public String getPageDescription() { return "Choose a name for your application and environment"; } }