/* * 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.configEditor; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.databinding.observable.Observables; import org.eclipse.core.databinding.observable.map.IObservableMap; import org.eclipse.core.databinding.observable.map.WritableMap; import org.eclipse.core.databinding.observable.set.WritableSet; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.swt.widgets.Display; import com.amazonaws.AmazonServiceException; import com.amazonaws.eclipse.core.AwsToolkitCore; import com.amazonaws.eclipse.core.ui.CancelableThread; import com.amazonaws.eclipse.elasticbeanstalk.ConfigurationOptionConstants; import com.amazonaws.eclipse.elasticbeanstalk.Environment; import com.amazonaws.services.elasticbeanstalk.AWSElasticBeanstalk; import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionDescription; import com.amazonaws.services.elasticbeanstalk.model.ConfigurationOptionSetting; import com.amazonaws.services.elasticbeanstalk.model.ConfigurationSettingsDescription; import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationOptionsRequest; import com.amazonaws.services.elasticbeanstalk.model.DescribeConfigurationOptionsResult; /** * Simple data model factory for environments. */ public class EnvironmentConfigDataModel { private static final Map<Environment, EnvironmentConfigDataModel> models = new HashMap<Environment, EnvironmentConfigDataModel>(); private final Environment environment; private final IObservableMap dataModel; private final Map<OptionKey, IObservableValue> sharedObservables; private final List<RefreshListener> listeners; private final List<ConfigurationOptionDescription> options; private final IgnoredOptions ignoredOptions; private CancelableThread refreshThread; private EnvironmentConfigDataModel(Environment environment) { this.environment = environment; this.dataModel = new WritableMap(); this.sharedObservables = new HashMap<OptionKey, IObservableValue>(); this.listeners = new LinkedList<RefreshListener>(); this.options = new LinkedList<ConfigurationOptionDescription>(); this.ignoredOptions = IgnoredOptions.getDefault(); } /** * Returns a data model for the environment given. */ public static synchronized EnvironmentConfigDataModel getInstance(Environment environment) { if ( !models.containsKey(environment) ) { models.put(environment, new EnvironmentConfigDataModel(environment)); } return models.get(environment); } /** * Returns the raw model, which is a typeless map. It's not generally safe * to observe this map further. * * @see EnvironmentConfigDataModel#observeEntry(ConfigurationOptionDescription) */ public IObservableMap getDataModel() { return dataModel; } /** * Observes the entry described by the key given. This must be used, rather * than observing the map entries directly, to ensure proper sharing of * observables. */ public synchronized IObservableValue observeEntry(ConfigurationOptionDescription key) { OptionKey optionKey = getKey(key); if ( !sharedObservables.containsKey(optionKey) ) { IObservableValue observable = Observables.observeMapEntry(dataModel, optionKey); sharedObservables.put(optionKey, observable); } return sharedObservables.get(optionKey); } /** * Returns the data model entry corresponding to the given key. */ public synchronized Object getEntry(ConfigurationOptionDescription key) { return dataModel.get(getKey(key)); } /** * Initializes the model given with the values provided, overwriting any * previous values. */ public synchronized void init(final Map<String, List<ConfigurationOptionSetting>> settings, final List<ConfigurationOptionDescription> options) { this.options.clear(); this.options.addAll(options); /* *TODO : There is a potential bug here. We should clear the all the contents in the dataModel first. * But it will make the editor dirty immediately. Hope in the future we can find a better way * to mark the edittor dirty. * */ for ( ConfigurationOptionDescription opt : options ) { if (!ignoredOptions.isNamespaceIgnored(opt.getNamespace())) { List<ConfigurationOptionSetting> settingsInNamespace = settings.get(opt.getNamespace()); if ( settingsInNamespace != null ) { for ( ConfigurationOptionSetting setting : settingsInNamespace ) { if ( opt.getName().equals(setting.getOptionName()) ) { String valueType = opt.getValueType(); OptionKey key = getKey(opt); if ( valueType.equals("Scalar") ) { dataModel.put(key, setting.getValue()); } else if ( valueType.equals("Boolean") ) { if (setting.getValue() != null) { dataModel.put(key, Boolean.valueOf(setting.getValue())); } } else if ( valueType.equals("List") ) { if ( !dataModel.containsKey(key) ) { dataModel.put(key, new WritableSet()); } synchronizeSets(setting, key); } else if ( valueType.equals("CommaSeparatedList") ) { dataModel.put(key, setting.getValue()); } else if ( valueType.equals("KeyValueList") ) { dataModel.put(key, setting.getValue()); } } } } } } } /** * Register a listener for refresh events. */ public synchronized void addRefreshListener(RefreshListener listener) { listeners.add(listener); } /** * Removes the given listener. */ public synchronized void removeRefreshListener(RefreshListener listener) { listeners.remove(listener); } /** * Asynchronously refreshes the model with services calls. To be notified of * refresh lifecycle events, register a listener. * * @param templateName * If non-null, will initialize using the values in the template * named, rather than the environment's running settings. */ public synchronized void refresh(String templateName) { cancelThread(refreshThread); refreshThread = new RefreshThread(templateName); refreshThread.start(); } public List<ConfigurationOptionDescription> getOptions() { return options; } /** * Synchronizes the two sets given in such as a way as to not provoke a set * changed event if their contents are equal. */ void synchronizeSets(ConfigurationOptionSetting setting, OptionKey key) { Set<String> settingValues = new HashSet<String>(); @SuppressWarnings("unchecked") Collection<String> modelValues = ((Collection<String>) dataModel.get(key)); /* * Sets a and b are equivalent iff for each element e in set a, * b.contains(e) and vice versa */ boolean setsEquivalent = true; String value = setting.getValue(); if ( value != null ) { for ( String v : value.split(",") ) { settingValues.add(v); if ( !modelValues.contains(v) ) { setsEquivalent = false; } } } for ( String v : modelValues ) { if ( !settingValues.contains(v) ) setsEquivalent = false; } if ( !setsEquivalent ) { modelValues.clear(); modelValues.addAll(settingValues); } } /** * Returns the current settings for this environment. * * @param templateName * If not null, get the settings for the template named, rather * than the running environment. */ public Map<String, List<ConfigurationOptionSetting>> getCurrentSettings(String templateName) { Map<String, List<ConfigurationOptionSetting>> settings; if ( templateName == null ) settings = getEnvironmentConfiguration(environment.getEnvironmentName()); else settings = getTemplateConfiguration(templateName); return settings; } /** * Returns a list of configuration options sorted first by namespace, then * by option name. */ public List<ConfigurationOptionDescription> getSortedConfigurationOptions() { AWSElasticBeanstalk client = AwsToolkitCore.getClientFactory(environment.getAccountId()) .getElasticBeanstalkClientByEndpoint(environment.getRegionEndpoint()); DescribeConfigurationOptionsResult optionsDesc = null; try { optionsDesc = client.describeConfigurationOptions(new DescribeConfigurationOptionsRequest() .withEnvironmentName(environment.getEnvironmentName())); } catch (AmazonServiceException e) { if ( "InvalidParameterValue".equals(e.getErrorCode()) ) { // If the environment doesn't exist yet... return new ArrayList<ConfigurationOptionDescription>(); } else { throw e; } } List<ConfigurationOptionDescription> options = new ArrayList<ConfigurationOptionDescription>(); for ( ConfigurationOptionDescription desc : optionsDesc.getOptions() ) { if (!ignoredOptions.isOptionIgnored(desc.getNamespace(), desc.getName())) { options.add(desc); } } Collections.sort(options, new Comparator<ConfigurationOptionDescription>() { public int compare(ConfigurationOptionDescription o1, ConfigurationOptionDescription o2) { if ( o1.getNamespace().equals(o2.getNamespace()) ) { return o1.getName().compareTo(o2.getName()); } else { return o1.getNamespace().compareTo(o2.getNamespace()); } } }); return options; } /** * Returns map of configuration option settings for an environment keyed by * their namespace. */ public Map<String, List<ConfigurationOptionSetting>> getEnvironmentConfiguration(String environmentName) { try { List<ConfigurationSettingsDescription> settings = environment.getCurrentSettings(); if ( settings.isEmpty() ) return null; return createSettingsMap(settings); } catch (AmazonServiceException ase) { if (ase.getErrorCode().equals("InvalidParameterValue")) return null; else throw ase; } } /** * Returns map of configuration option settings for a template keyed by * their namespace. */ public Map<String, List<ConfigurationOptionSetting>> getTemplateConfiguration(String templateName) { List<ConfigurationSettingsDescription> settings = environment.getCurrentSettings(); if ( settings.isEmpty() ) return null; return createSettingsMap(settings); } /** * Sorts the list of settings given into a map keyed by namespace. */ public Map<String, List<ConfigurationOptionSetting>> createSettingsMap(List<ConfigurationSettingsDescription> settings) { Map<String, List<ConfigurationOptionSetting>> options = new HashMap<String, List<ConfigurationOptionSetting>>(); for ( ConfigurationOptionSetting opt : settings.get(0).getOptionSettings() ) { if ( !options.containsKey(opt.getNamespace()) ) { options.put(opt.getNamespace(), new ArrayList<ConfigurationOptionSetting>()); } options.get(opt.getNamespace()).add(opt); } return options; } /** * Transforms the model into a list of configuration option settings. */ public Collection<ConfigurationOptionSetting> createConfigurationOptions() { Collection<ConfigurationOptionSetting> settings = new ArrayList<ConfigurationOptionSetting>(); for ( Object key : dataModel.keySet() ) { OptionKey option = (OptionKey) key; Object value = dataModel.get(key); if ( value != null ) { if ( value instanceof Collection ) { @SuppressWarnings("rawtypes") Collection collection = (Collection) value; if ( !collection.isEmpty() ) { ConfigurationOptionSetting setting = new ConfigurationOptionSetting() .withNamespace(option.namespace).withOptionName(option.name) .withValue(join(collection)); settings.add(setting); } } else { ConfigurationOptionSetting setting = new ConfigurationOptionSetting() .withNamespace(option.namespace).withOptionName(option.name).withValue(value.toString()); settings.add(setting); } } } return settings; } /** * Joins the collection given with commas. */ @SuppressWarnings("rawtypes") private String join(Collection list) { StringBuilder b = new StringBuilder(); boolean seenOne = false; for ( Object o : list ) { if ( seenOne ) b.append(","); else seenOne = true; b.append(o); } return b.toString(); } private OptionKey getKey(ConfigurationOptionDescription key) { return new OptionKey(key); } /** * Cancels the thread given if it's running. */ protected void cancelThread(CancelableThread thread) { if ( thread != null ) { synchronized (thread) { if ( thread.isRunning() ) { thread.cancel(); } } } } private final class RefreshThread extends CancelableThread { private String template; public RefreshThread(String template) { this.template = template; } @Override public void run() { try { for ( RefreshListener listener : listeners ) { listener.refreshStarted(); } final List<ConfigurationOptionDescription> options = getSortedConfigurationOptions(); final Map<String, List<ConfigurationOptionSetting>> settings = getCurrentSettings(template); Display.getDefault().syncExec(new Runnable() { public void run() { synchronized (RefreshThread.this) { if ( !isCanceled() ) init(settings, options); } } }); } catch ( Exception e ) { for ( RefreshListener listener : listeners ) { listener.refreshError(e); } return; } for ( RefreshListener listener : listeners ) { listener.refreshFinished(); } } } /** * We can't use the SDK's data types as map keys because they don't .equals * one another. This is a lightweight adapter to allow them to be used as * such, as well as being reversible to an option name. */ public static final class OptionKey { private final String name; private final String namespace; public OptionKey(ConfigurationOptionDescription opt) { this.name = opt.getName(); this.namespace = opt.getNamespace(); } @Override public boolean equals(Object o2) { if ( o2 instanceof OptionKey == false ) return false; return ((OptionKey) o2).name.equals(this.name) && ((OptionKey) o2).namespace.equals(this.namespace); } @Override public int hashCode() { return name.hashCode() + namespace.hashCode(); } @Override public String toString() { return namespace + ":" + name; } } /** * Container for ignored namespaces and options */ public static class IgnoredOptions { static final String IGNORE_ALL_OPTIONS = "aws:eclipse:toolkit:IGNORE_ALL_OPTIONS_IN_NAMESPACE"; private final Map<String, List<String>> ignoredOptions; public static IgnoredOptions getDefault() { IgnoredOptions options = new IgnoredOptions(new HashMap<String, List<String>>()); options.ignoreNamespace("aws:cloudformation:template:parameter"); options.ignoreNamespace("aws:ec2:vpc"); options.ignoreOption(ConfigurationOptionConstants.HEALTH_REPORTING_SYSTEM, "ConfigDocument"); return options; } public IgnoredOptions(Map<String, List<String>> ignoredOptions) { this.ignoredOptions = ignoredOptions; } public boolean isOptionIgnored(String namespace, String optionName) { return isNamespaceIgnored(namespace) || containsOption(namespace, optionName); } public boolean isNamespaceIgnored(String namespace) { return containsOption(namespace, IGNORE_ALL_OPTIONS); } private boolean containsOption(String namespace, String option) { if (!ignoredOptions.containsKey(namespace)) { return false; } return ignoredOptions.get(namespace).contains(option); } void ignoreOption(String namespace, String optionName) { if (ignoredOptions.get(namespace) == null) { ignoredOptions.put(namespace, new LinkedList<String>()); } ignoredOptions.get(namespace).add(optionName); } void ignoreNamespace(String namespace) { ignoreOption(namespace, IGNORE_ALL_OPTIONS); } } }