com.amazonaws.eclipse.core.ui.preferences.AwsAccountPreferencePage.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.eclipse.core.ui.preferences.AwsAccountPreferencePage.java

Source

/*
 * Copyright 2008-2011 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.core.ui.preferences;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;

import org.eclipse.jface.preference.FileFieldEditor;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.StringFieldEditor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
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.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
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 org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.events.IExpansionListener;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.Section;

import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.AwsUrls;
import com.amazonaws.eclipse.core.preferences.PreferenceConstants;
import com.amazonaws.eclipse.core.ui.PreferenceLinkListener;
import com.amazonaws.eclipse.core.ui.WebLinkListener;
import com.amazonaws.util.StringUtils;

/**
 * Preference page for basic AWS account information.
 */
public class AwsAccountPreferencePage extends AwsToolkitPreferencePage implements IWorkbenchPreferencePage {

    public static final String ID = "com.amazonaws.eclipse.core.ui.preferences.AwsAccountPreferencePage";

    // State variables to simplify bookkeeping
    private String currentAccountId;
    private final Map<String, String> accountNamesByIdentifier = new HashMap<String, String>();

    // Page controls
    private Combo accountSelector;
    private AccountStringFieldEditor accountNameFieldEditor;
    private AccountStringFieldEditor userIdFieldEditor;
    private AccountStringFieldEditor accessKeyFieldEditor;
    private AccountStringFieldEditor secretKeyFieldEditor;
    private AccountFileFieldEditor certificateFieldEditor;
    private AccountFileFieldEditor certificatePrivateKeyFieldEditor;

    /*
     * The set of field editors that need to know when the account or its name
     * changes.
     */
    private final Collection<AccountFieldEditor> accountFieldEditors = new LinkedList<AccountFieldEditor>();

    /** The checkbox controlling how we display the secret key */
    private Button hideSecretKeyCheckbox;

    /** The Text control in the secret key field editor */
    private Text secretKeyText;

    private Font italicFont;

    /**
     * Creates the preference page and connects it to the plugin's preference
     * store.
     */
    public AwsAccountPreferencePage() {
        super("AWS Toolkit Preferences");

        setPreferenceStore(AwsToolkitCore.getDefault().getPreferenceStore());
        setDescription("AWS Toolkit Preferences");
    }

    /*
     * PreferencePage Interface
     */

    /*
     * (non-Javadoc)
     *
     * @see
     * org.eclipse.ui.IWorkbenchPreferencePage#init(org.eclipse.ui.IWorkbench)
     */
    public void init(IWorkbench workbench) {
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * org.eclipse.jface.preference.PreferencePage#createContents(org.eclipse
     * .swt.widgets.Composite)
     */
    @Override
    protected Control createContents(Composite parent) {
        currentAccountId = getPreferenceStore().getString(PreferenceConstants.P_CURRENT_ACCOUNT);

        accountFieldEditors.clear();
        accountNamesByIdentifier.clear();
        accountNamesByIdentifier.putAll(getAccounts(getPreferenceStore()));

        Composite composite = new Composite(parent, SWT.NONE);
        composite.setLayout(new GridLayout());
        GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
        composite.setLayoutData(gridData);

        WebLinkListener webLinkListener = new WebLinkListener();

        Group accountsGroup = createAccountSelector(composite);
        Group awsGroup = createAwsAccountSection(accountsGroup, webLinkListener);
        createOptionalSection(awsGroup, webLinkListener);

        Link networkConnectionLink = new Link(composite, SWT.NULL);
        networkConnectionLink.setText(
                "See <a href=\"org.eclipse.ui.net.NetPreferences\">Network connections</a> to configure how the AWS Toolkit connects to the internet.");
        PreferenceLinkListener preferenceLinkListener = new PreferenceLinkListener();
        networkConnectionLink.addListener(SWT.Selection, preferenceLinkListener);

        String javaForumLinkText = "Get help or provide feedback on the " + "<a href=\""
                + AwsUrls.JAVA_DEVELOPMENT_FORUM_URL + "\">AWS Java Development forum</a>. ";
        newLink(webLinkListener, javaForumLinkText, composite);

        return composite;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.jface.preference.PreferencePage#performDefaults()
     */
    @Override
    protected void performDefaults() {
        userIdFieldEditor.loadDefault();
        accessKeyFieldEditor.loadDefault();
        secretKeyFieldEditor.loadDefault();

        certificateFieldEditor.loadDefault();
        certificatePrivateKeyFieldEditor.loadDefault();

        super.performDefaults();
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.jface.preference.PreferencePage#performOk()
     */
    @Override
    public boolean performOk() {
        // Clean up the AWS User ID and store it
        String userId = userIdFieldEditor.getStringValue();
        userId = userId.replace("-", "");
        userId = userId.replace(" ", "");
        userIdFieldEditor.setStringValue(userId);
        userIdFieldEditor.store();

        // Clean up the account name and store it
        String accountName = accountNameFieldEditor.getStringValue();
        accountName = accountName.trim();
        accountNameFieldEditor.setStringValue(accountName);
        accountNameFieldEditor.store();

        // Clean up the AWS Access Key and store it
        String accessKey = accessKeyFieldEditor.getStringValue();
        accessKey = accessKey.trim();
        accessKeyFieldEditor.setStringValue(accessKey);
        accessKeyFieldEditor.store();

        // Clean up the AWS Secret Key and store it
        String secretKey = secretKeyFieldEditor.getStringValue();
        secretKey = secretKey.trim();
        secretKeyFieldEditor.setStringValue(secretKey);
        secretKeyFieldEditor.store();

        // Save the certificate and private key
        certificateFieldEditor.store();
        certificatePrivateKeyFieldEditor.store();

        // Finally update the list of account identifiers and the current
        // account
        String accountIds = StringUtils.join(PreferenceConstants.ACCOUNT_ID_SEPARATOR,
                accountNamesByIdentifier.keySet().toArray(new String[accountNamesByIdentifier.size()]));
        getPreferenceStore().setValue(PreferenceConstants.P_ACCOUNT_IDS, accountIds);

        getPreferenceStore().setValue(PreferenceConstants.P_CURRENT_ACCOUNT, currentAccountId);

        return super.performOk();
    }

    /*
     * Private Interface
     */

    /**
     * Creates the account selection section.
     */
    private Group createAccountSelector(final Composite parent) {
        Group accountSelectionGroup = new Group(parent, SWT.NONE);
        accountSelectionGroup.setText("AWS Accounts:");
        accountSelectionGroup.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
        accountSelectionGroup.setLayout(new GridLayout(4, false));

        new Label(accountSelectionGroup, SWT.READ_ONLY).setText("Default Account: ");
        accountSelector = new Combo(accountSelectionGroup, SWT.DROP_DOWN | SWT.READ_ONLY);
        accountSelector.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

        IPreferenceStore preferenceStore = getPreferenceStore();
        Map<String, String> accounts = getAccounts(preferenceStore);

        List<String> nameList = new ArrayList<String>();
        nameList.addAll(accounts.values());
        Collections.sort(nameList);

        int selectedIndex = 0;
        for (int i = 0; i < nameList.size(); i++) {
            if (nameList.get(i).equals(accountNamesByIdentifier.get(currentAccountId))) {
                selectedIndex = i;
                break;
            }
        }

        accountSelector.setItems(nameList.toArray(new String[nameList.size()]));
        accountSelector.select(selectedIndex);

        accountSelector.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                String accountName = accountSelector.getItem(accountSelector.getSelectionIndex());
                String id = reverseAccountLookup(accountName);
                accountChanged(id);
            }
        });

        final Button addNewAccount = new Button(accountSelectionGroup, SWT.PUSH);
        addNewAccount.setText("Add account");
        final Button deleteAccount = new Button(accountSelectionGroup, SWT.PUSH);
        deleteAccount.setText("Remove account");
        deleteAccount.setEnabled(accounts.size() > 1);

        Label defaultAccountExplainationLabel = new Label(accountSelectionGroup, SWT.WRAP);
        defaultAccountExplainationLabel.setText(
                "This account will be used by default for all features in the AWS Toolkit for Eclipse, unless they specifically allow you to select an account.");
        FontData[] fontData = defaultAccountExplainationLabel.getFont().getFontData();
        for (FontData fd : fontData) {
            fd.setStyle(SWT.ITALIC);
        }
        italicFont = new Font(Display.getDefault(), fontData);
        defaultAccountExplainationLabel.setFont(italicFont);

        GridData layoutData = new GridData(SWT.FILL, SWT.TOP, true, false);
        layoutData.horizontalSpan = 4;
        layoutData.widthHint = 200;
        defaultAccountExplainationLabel.setLayoutData(layoutData);

        addNewAccount.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                String[] currentAccountNames = accountSelector.getItems();
                String[] newAccountNames = new String[currentAccountNames.length + 1];
                System.arraycopy(currentAccountNames, 0, newAccountNames, 0, currentAccountNames.length);
                String newAccountName = "New Account";
                newAccountNames[currentAccountNames.length] = newAccountName;
                accountSelector.setItems(newAccountNames);
                accountSelector.select(currentAccountNames.length);
                accountSelector.getParent().getParent().layout();

                String newAccountId = UUID.randomUUID().toString();
                accountNamesByIdentifier.put(newAccountId, newAccountName);

                accountChanged(newAccountId);
                accountNameFieldEditor.setStringValue(newAccountName);

                deleteAccount.setEnabled(true);
            }
        });

        deleteAccount.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                String[] currentAccountNames = accountSelector.getItems();
                String[] newAccountNames = new String[currentAccountNames.length - 1];
                int i = 0;
                int j = 0;
                for (String accountName : currentAccountNames) {
                    if (i++ != accountSelector.getSelectionIndex()) {
                        newAccountNames[j++] = accountName;
                    }
                }
                if (newAccountNames.length == 0)
                    newAccountNames = new String[] { PreferenceConstants.DEFAULT_ACCOUNT_NAME };

                accountSelector.setItems(newAccountNames);
                accountSelector.select(0);
                accountNamesByIdentifier.remove(currentAccountId);

                accountChanged(reverseAccountLookup(newAccountNames[0]));

                deleteAccount.setEnabled(newAccountNames.length > 1);
            }
        });

        return accountSelectionGroup;
    }

    @Override
    public void dispose() {
        if (italicFont != null)
            italicFont.dispose();
        super.dispose();
    }

    /**
     * Looks up an account Id by its name.
     */
    protected String reverseAccountLookup(String accountName) {
        String id = null;
        for (Entry<String, String> entry : accountNamesByIdentifier.entrySet()) {
            if (entry.getValue().equals(accountName)) {
                id = entry.getKey();
            }
        }
        return id;
    }

    /**
     * Returns the list of account names registered. If none are registered yet,
     * returns the default one.
     *
     * @param preferenceStore
     *            The preference store to use when looking up the account names.
     * @return A map of account identifier to customer-assigned names. The
     *         identifiers are the primary, immutable key used to access the
     *         account.
     */
    public static Map<String, String> getAccounts(IPreferenceStore preferenceStore) {
        String accountNamesString = preferenceStore.getString(PreferenceConstants.P_ACCOUNT_IDS);

        // bootstrapping
        if (accountNamesString == null || accountNamesString.length() == 0) {
            String id = UUID.randomUUID().toString();
            preferenceStore.putValue(PreferenceConstants.P_CURRENT_ACCOUNT, id);
            preferenceStore.putValue(PreferenceConstants.P_ACCOUNT_IDS, id);
            preferenceStore.putValue(id + ":" + PreferenceConstants.P_ACCOUNT_NAME,
                    PreferenceConstants.DEFAULT_ACCOUNT_NAME_BASE_64);
        }

        String[] accountIds = accountNamesString.split(PreferenceConstants.ACCOUNT_ID_SEPARATOR_REGEX);
        Map<String, String> names = new HashMap<String, String>();
        for (String id : accountIds) {
            String preferenceName = id + ":" + PreferenceConstants.P_ACCOUNT_NAME;
            names.put(id, ObfuscatingStringFieldEditor.decodeString(preferenceStore.getString(preferenceName)));
        }

        return names;
    }

    /**
     * Creates the widgets for the AWS account information section on this
     * preference page.
     *
     * @param parent
     *            The parent preference page composite.
     * @param webLinkListener
     *            The listener to attach to links.
     */
    private Group createAwsAccountSection(final Composite parent, WebLinkListener webLinkListener) {

        final Group awsAccountGroup = new Group(parent, SWT.NONE);
        GridData gridData1 = new GridData(SWT.FILL, SWT.TOP, true, false);
        gridData1.horizontalSpan = 4;
        gridData1.verticalIndent = 10;
        awsAccountGroup.setLayoutData(gridData1);
        awsAccountGroup.setText("Account Details:");

        String linkText = "<a href=\"" + AwsUrls.SIGN_UP_URL + "\">Sign up for a new AWS account</a> or "
                + "<a href=\"" + AwsUrls.SECURITY_CREDENTIALS_URL
                + "\">find your existing AWS security credentials</a>.";
        newLink(webLinkListener, linkText, awsAccountGroup);
        createSpacer(awsAccountGroup);

        accountNameFieldEditor = newStringFieldEditor(currentAccountId, PreferenceConstants.P_ACCOUNT_NAME,
                "Account &Name:", awsAccountGroup);

        accountNameFieldEditor.getTextControl(awsAccountGroup).addModifyListener(new ModifyListener() {

            public void modifyText(ModifyEvent e) {
                String newAccountName = accountNameFieldEditor.getStringValue();
                // On linux, we have to select an index after setting the current item
                int selectionIndex = accountSelector.getSelectionIndex();
                accountSelector.setItem(selectionIndex, newAccountName);
                accountSelector.getParent().getParent().layout();
                accountSelector.select(selectionIndex);
                accountNamesByIdentifier.put(currentAccountId, newAccountName);
                updatePageValidation();
            }
        });

        accessKeyFieldEditor = newStringFieldEditor(currentAccountId, PreferenceConstants.P_ACCESS_KEY,
                "&Access Key ID:", awsAccountGroup);
        secretKeyFieldEditor = newStringFieldEditor(currentAccountId, PreferenceConstants.P_SECRET_KEY,
                "&Secret Access Key:", awsAccountGroup);

        // create an empty label in the first column so that the hide secret key
        // checkbox lines up with the other text controls
        new Label(awsAccountGroup, SWT.NONE);

        hideSecretKeyCheckbox = new Button(awsAccountGroup, SWT.CHECK);
        hideSecretKeyCheckbox.setText("Show secret access key");
        hideSecretKeyCheckbox.setSelection(false);
        GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
        gridData.horizontalSpan = 2;
        gridData.verticalIndent = -6;
        gridData.horizontalIndent = 3;
        hideSecretKeyCheckbox.setLayoutData(gridData);
        hideSecretKeyCheckbox.addListener(SWT.Selection, new Listener() {

            public void handleEvent(Event event) {
                updateSecretKeyText();
            }
        });

        secretKeyText = secretKeyFieldEditor.getTextControl(awsAccountGroup);

        updateSecretKeyText();
        tweakLayout((GridLayout) awsAccountGroup.getLayout());

        accountFieldEditors.add(accountNameFieldEditor);
        accountFieldEditors.add(accessKeyFieldEditor);
        accountFieldEditors.add(secretKeyFieldEditor);

        return awsAccountGroup;
    }

    /**
     * Invoked whenever the selected account information changes.
     */
    private void accountChanged(String accountIdentifier) {
        currentAccountId = accountIdentifier;
        for (AccountFieldEditor editor : accountFieldEditors) {
            editor.accountChanged(accountIdentifier);
        }
    }

    /**
     * Update or clear the error message and validity of the page.
     */
    private void updatePageValidation() {
        String errorString = validate();
        if (errorString != null) {
            setValid(false);
            setErrorMessage(errorString);
            return;
        }

        setValid(true);
        setErrorMessage(null);
    }

    /**
     * Returns an error message if there's a problem with the page's fields, or
     * null if there are no errors.
     */
    private String validate() {
        Set<String> accountNames = new HashSet<String>();

        if (accountSelector != null) {
            for (String accountName : accountSelector.getItems()) {
                if (!accountNames.add(accountName)) {
                    return "Duplicate account name defined";
                }
            }
        }

        if (accountNameFieldEditor != null) {
            if (accountNameFieldEditor.getStringValue().trim().length() == 0) {
                return "Account name must not be blank";
            }

            for (Entry<String, String> entry : accountNameFieldEditor.valuesByAccountIdentifier.entrySet()) {
                if (!entry.getKey().equals(currentAccountId))
                    if (entry.getValue().length() == 0)
                        return "Account name must not be blank";
            }
        }

        if (certificateFieldEditor != null) {
            if (invalidFile(certificateFieldEditor.getStringValue())) {
                return "Certificate file does not exist";
            }

            for (Entry<String, String> entry : certificateFieldEditor.valuesByAccountIdentifier.entrySet()) {
                if (!entry.getKey().equals(currentAccountId))
                    if (invalidFile(entry.getValue()))
                        return "Certificate file does not exist";
            }
        }

        if (certificatePrivateKeyFieldEditor != null) {
            String certFile = certificatePrivateKeyFieldEditor.getStringValue();
            if (invalidFile(certFile)) {
                return "Private key file does not exist";
            }

            for (Entry<String, String> entry : certificatePrivateKeyFieldEditor.valuesByAccountIdentifier
                    .entrySet()) {
                if (!entry.getKey().equals(currentAccountId))
                    if (invalidFile(entry.getValue()))
                        return "Private key file does not exist";
            }
        }

        return null;
    }

    protected boolean invalidFile(String certFile) {
        return certFile.trim().length() > 0 && !new File(certFile).exists();
    }

    /**
     * Creates a new label to serve as an example for a field, using the
     * specified text. The label will be displayed with a subtle font. This
     * method assumes that the grid layout for the specified composite contains
     * three columns.
     *
     * @param composite
     *            The parent component for this new widget.
     * @param text
     *            The example text to display in the new label.
     */
    private void createFieldExampleLabel(Composite composite, String text) {
        Label label = new Label(composite, SWT.NONE);
        Font font = label.getFont();

        label = new Label(composite, SWT.NONE);
        label.setText(text);

        FontData[] fontData = font.getFontData();
        if (fontData.length > 0) {
            FontData fd = fontData[0];
            fd.setHeight(10);
            fd.setStyle(SWT.ITALIC);

            label.setFont(new Font(Display.getCurrent(), fd));
        }

        GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
        gridData.horizontalSpan = 2;
        gridData.verticalAlignment = SWT.TOP;
        gridData.horizontalIndent = 3;
        gridData.verticalIndent = -4;
        label.setLayoutData(gridData);
    }

    /**
     * Updates the secret key text according to whether or not the
     * "display secret key in plain text" checkbox is selected or not.
     */
    private void updateSecretKeyText() {
        if (hideSecretKeyCheckbox == null)
            return;
        if (secretKeyText == null)
            return;

        if (hideSecretKeyCheckbox.getSelection()) {
            secretKeyText.setEchoChar('\0');
        } else {
            secretKeyText.setEchoChar('*');
        }
    }

    /**
     * Creates the widgets for the optional configuration section on this
     * preference page.
     *
     * @param parent
     *            The parent preference page composite.
     * @param webLinkListener
     *            The listener to attach to links.
     */
    private void createOptionalSection(final Composite parent, WebLinkListener webLinkListener) {

        Section expoandableComposite = new Section(parent, ExpandableComposite.TWISTIE);
        GridData gd = new GridData(SWT.FILL, SWT.TOP, true, false);
        gd.horizontalSpan = LAYOUT_COLUMN_WIDTH;
        expoandableComposite.setLayoutData(gd);

        Composite optionalConfigGroup = new Composite(expoandableComposite, SWT.NONE);
        optionalConfigGroup.setLayout(new GridLayout());
        optionalConfigGroup.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));

        expoandableComposite.setClient(optionalConfigGroup);
        expoandableComposite.setText("Optional configuration:");
        expoandableComposite.setExpanded(false);

        expoandableComposite.addExpansionListener(new IExpansionListener() {

            public void expansionStateChanging(ExpansionEvent e) {
            }

            public void expansionStateChanged(ExpansionEvent e) {
                parent.getParent().layout();
            }
        });

        String linkText = "Your AWS account number and X.509 certificate are only needed if you want to bundle EC2 instances from Eclipse.  "
                + "<a href=\"" + AwsUrls.SECURITY_CREDENTIALS_URL + "\">Manage your AWS X.509 certificate</a>.";
        newLink(webLinkListener, linkText, optionalConfigGroup);

        createSpacer(optionalConfigGroup);

        userIdFieldEditor = newStringFieldEditor(currentAccountId, PreferenceConstants.P_USER_ID,
                "Account &Number:", optionalConfigGroup);
        createFieldExampleLabel(optionalConfigGroup, "ex: 1111-2222-3333");

        certificateFieldEditor = newFileFieldEditor(currentAccountId, PreferenceConstants.P_CERTIFICATE_FILE,
                "&Certificate File:", optionalConfigGroup);
        certificatePrivateKeyFieldEditor = newFileFieldEditor(currentAccountId,
                PreferenceConstants.P_PRIVATE_KEY_FILE, "&Private Key File:", optionalConfigGroup);

        tweakLayout((GridLayout) optionalConfigGroup.getLayout());

        accountFieldEditors.add(userIdFieldEditor);
        accountFieldEditors.add(certificateFieldEditor);
        accountFieldEditors.add(certificatePrivateKeyFieldEditor);
    }

    protected AccountStringFieldEditor newStringFieldEditor(String currentAccount, String preferenceKey,
            String label, Composite parent) {
        AccountStringFieldEditor fieldEditor = new AccountStringFieldEditor(currentAccount, preferenceKey, label,
                parent);
        setUpFieldEditor(currentAccount, preferenceKey, parent, fieldEditor);
        return fieldEditor;
    }

    protected AccountFileFieldEditor newFileFieldEditor(String currentAccount, String preferenceKey, String label,
            Composite parent) {
        AccountFileFieldEditor fieldEditor = new AccountFileFieldEditor(currentAccount, preferenceKey, label,
                parent);
        setUpFieldEditor(currentAccount, preferenceKey, parent, fieldEditor);
        return fieldEditor;
    }

    protected void setUpFieldEditor(String currentAccount, String preferenceKey, Composite parent,
            StringFieldEditor fieldEditor) {
        fieldEditor.setPage(this);
        fieldEditor.setPreferenceStore(this.getPreferenceStore());
        fieldEditor.load();

        // For backwards compatibility with single-account storage
        if (accountNamesByIdentifier.get(currentAccount) != null
                && accountNamesByIdentifier.get(currentAccount).equals(PreferenceConstants.DEFAULT_ACCOUNT_NAME)
                && (fieldEditor.getStringValue() == null || fieldEditor.getStringValue().length() == 0)) {
            String currentPrefValue = getPreferenceStore().getString(preferenceKey);
            if (ObfuscatingStringFieldEditor.isBase64(currentPrefValue)) {
                currentPrefValue = ObfuscatingStringFieldEditor.decodeString(currentPrefValue);
            }
            fieldEditor.setStringValue(currentPrefValue);
        }

        fieldEditor.fillIntoGrid(parent, LAYOUT_COLUMN_WIDTH);
        fieldEditor.getTextControl(parent).addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent e) {
                updatePageValidation();
            }
        });
    }

    private interface AccountFieldEditor {

        /**
         * Invoked when the current account changes.
         */
        public void accountChanged(String accountIdentifier);
    }

    /*
     * The next two classes are duplicates, but since their hierarchies diverge
     * there isn't much we can do about that.
     */

    /**
     * Preference editor with a dynamic key name
     */
    private class AccountStringFieldEditor extends ObfuscatingStringFieldEditor implements AccountFieldEditor {

        private String accountIdentifier;

        private final Map<String, String> valuesByAccountIdentifier = new HashMap<String, String>();

        public AccountStringFieldEditor(String accountIdentifier, String preferenceKey, String labelText,
                Composite parent) {
            super(preferenceKey, labelText, parent);
            this.accountIdentifier = accountIdentifier;
        }

        @Override
        public String getPreferenceName() {
            return accountIdentifier + ":" + super.getPreferenceName();
        }

        /**
         * Store the current value of the field before replacing the contents
         * with either the last user-entered value or else the persisted
         * preference's value.
         */
        public void accountChanged(String accountIdentifier) {
            valuesByAccountIdentifier.put(this.accountIdentifier, getStringValue());
            this.accountIdentifier = accountIdentifier;
            if (valuesByAccountIdentifier.containsKey(accountIdentifier)) {
                this.setStringValue(valuesByAccountIdentifier.get(accountIdentifier));
            } else {
                this.load();
            }
        }

        /**
         * Stores all the values that have been edited, not just the current one.
         */
        @Override
        protected void doStore() {
            valuesByAccountIdentifier.put(accountIdentifier, getStringValue());
            String originalAccountIdentifier = this.accountIdentifier;

            for (Entry<String, String> entry : valuesByAccountIdentifier.entrySet()) {
                this.accountIdentifier = entry.getKey();
                getTextControl().setText(entry.getValue());
                super.doStore();
            }

            this.accountIdentifier = originalAccountIdentifier;
            getTextControl().setText(valuesByAccountIdentifier.get(accountIdentifier));
        }

        @Override
        protected boolean checkState() {
            String errorMessage = validate();
            setErrorMessage(errorMessage);
            return errorMessage == null;
        }

        @Override
        protected void clearErrorMessage() {
        }
    }

    /**
     * Preference editor with a dynamic key name
     */
    private final class AccountFileFieldEditor extends FileFieldEditor implements AccountFieldEditor {

        private String accountIdentifier;

        private final Map<String, String> valuesByAccountIdentifier = new HashMap<String, String>();

        public AccountFileFieldEditor(String accountIdentifier, String preferenceKey, String labelText,
                Composite parent) {
            super(preferenceKey, labelText, parent);
            this.accountIdentifier = accountIdentifier;
        }

        @Override
        public String getPreferenceName() {
            return accountIdentifier + ":" + super.getPreferenceName();
        }

        /**
         * Store the current value of the field before replacing the contents
         * with either the last user-entered value or else the persisted
         * preference's value.
         */
        public void accountChanged(String accountIdentifier) {
            valuesByAccountIdentifier.put(this.accountIdentifier, getStringValue());
            this.accountIdentifier = accountIdentifier;
            if (valuesByAccountIdentifier.containsKey(accountIdentifier)) {
                this.setStringValue(valuesByAccountIdentifier.get(accountIdentifier));
            } else {
                this.load();
            }
        }

        /**
         * Stores all the values that have been edited, not just the current one.
         */
        @Override
        protected void doStore() {
            valuesByAccountIdentifier.put(accountIdentifier, getStringValue());
            String originalAccountIdentifier = this.accountIdentifier;

            for (Entry<String, String> entry : valuesByAccountIdentifier.entrySet()) {
                this.accountIdentifier = entry.getKey();
                getTextControl().setText(entry.getValue());
                super.doStore();
            }

            this.accountIdentifier = originalAccountIdentifier;
            getTextControl().setText(valuesByAccountIdentifier.get(accountIdentifier));
        }

        @Override
        protected boolean checkState() {
            String errorMessage = validate();
            setErrorMessage(errorMessage);
            return errorMessage == null;
        }

        @Override
        protected void clearErrorMessage() {
        }
    }
}