/*
 * JetS3t : Java S3 Toolkit
 * Project hosted at http://bitbucket.org/jmurty/jets3t/
 *
 * Copyright 2006-2010 James Murty
 *
 * 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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 org.jets3t.apps.uploader;

import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import javax.swing.BorderFactory;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.Border;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodRetryHandler;
import org.apache.commons.httpclient.NTCredentials;
import org.apache.commons.httpclient.ProxyHost;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScheme;
import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
import org.apache.commons.httpclient.auth.CredentialsProvider;
import org.apache.commons.httpclient.auth.NTLMScheme;
import org.apache.commons.httpclient.auth.RFC2617Scheme;
import org.apache.commons.httpclient.contrib.proxy.PluginProxyUtil;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jets3t.gui.AuthenticationDialog;
import org.jets3t.gui.ErrorDialog;
import org.jets3t.gui.GuiUtils;
import org.jets3t.gui.HyperlinkActivatedListener;
import org.jets3t.gui.JHtmlLabel;
import org.jets3t.gui.UserInputFields;
import org.jets3t.gui.skins.SkinsFactory;
import org.jets3t.service.Constants;
import org.jets3t.service.Jets3tProperties;
import org.jets3t.service.S3ServiceException;
import org.jets3t.service.impl.rest.httpclient.RestS3Service;
import org.jets3t.service.io.BytesProgressWatcher;
import org.jets3t.service.io.ProgressMonitoredInputStream;
import org.jets3t.service.model.S3Object;
import org.jets3t.service.multithread.CancelEventTrigger;
import org.jets3t.service.multithread.CopyObjectsEvent;
import org.jets3t.service.multithread.CreateBucketsEvent;
import org.jets3t.service.multithread.CreateObjectsEvent;
import org.jets3t.service.multithread.DeleteObjectsEvent;
import org.jets3t.service.multithread.DeleteVersionedObjectsEvent;
import org.jets3t.service.multithread.DownloadObjectsEvent;
import org.jets3t.service.multithread.GetObjectHeadsEvent;
import org.jets3t.service.multithread.GetObjectsEvent;
import org.jets3t.service.multithread.ListObjectsEvent;
import org.jets3t.service.multithread.LookupACLEvent;
import org.jets3t.service.multithread.S3ServiceEventListener;
import org.jets3t.service.multithread.S3ServiceMulti;
import org.jets3t.service.multithread.ServiceEvent;
import org.jets3t.service.multithread.ThreadWatcher;
import org.jets3t.service.multithread.UpdateACLEvent;
import org.jets3t.service.security.AWSCredentials;
import org.jets3t.service.utils.ByteFormatter;
import org.jets3t.service.utils.Mimetypes;
import org.jets3t.service.utils.ServiceUtils;
import org.jets3t.service.utils.TimeFormatter;
import org.jets3t.service.utils.gatekeeper.GatekeeperMessage;
import org.jets3t.service.utils.gatekeeper.SignatureRequest;
import org.jets3t.service.utils.signedurl.SignedUrlAndObject;

import com.centerkey.utils.BareBonesBrowserLaunch;

/**
 * Dual application and applet for uploading files and XML metadata information to the Amazon S3
 * service.
 * <p>
 * For more information and help please see the
 * <a href="http://www.jets3t.org/applications/uploader.html">Uploader Guide</a>.
 * </p>
 * <p>
 * The Uploader is highly configurable through properties specified in a file
 * <tt>uploader.properties</tt>. This file <b>must be available</b> at the root of the classpath.
 *
 * @author James Murty
 */
public class Uploader extends JApplet implements S3ServiceEventListener, ActionListener, ListSelectionListener, HyperlinkActivatedListener, CredentialsProvider {
    private static final long serialVersionUID = 2759324769352022783L;

    private static final Log log = LogFactory.getLog(Uploader.class);

    public static final String APPLICATION_DESCRIPTION = "Uploader/" + Constants.JETS3T_VERSION;

    public static final String UPLOADER_PROPERTIES_FILENAME = "uploader.properties";

    private static final String UPLOADER_VERSION_ID = "JetS3t Uploader/" + Constants.JETS3T_VERSION;

    public static final int WIZARD_SCREEN_1 = 1;
    public static final int WIZARD_SCREEN_2 = 2;
    public static final int WIZARD_SCREEN_3 = 3;
    public static final int WIZARD_SCREEN_4 = 4;
    public static final int WIZARD_SCREEN_5 = 5;

    /*
     * Error codes displayed when errors occur. Each possbile error condition has its own code
     * to aid in resolving user's problems.
     */
    public static final String ERROR_CODE__MISSING_REQUIRED_PARAM = "100";
    public static final String ERROR_CODE__S3_UPLOAD_FAILED = "101";
    public static final String ERROR_CODE__UPLOAD_REQUEST_DECLINED = "102";
    public static final String ERROR_CODE__TRANSACTION_ID_REQUIRED_TO_CREATE_XML_SUMMARY = "103";

    /*
     * HTTP connection settings for communication *with Gatekeeper only*, the
     * S3 connection parameters are set in the jets3t.properties file.
     */
    public static final int HTTP_CONNECTION_TIMEOUT = 60000;
    public static final int SOCKET_CONNECTION_TIMEOUT = 60000;
    public static final int MAX_CONNECTION_RETRIES = 5;


    private Frame ownerFrame = null;

    private UserInputFields userInputFields = null;
    private Properties userInputProperties = null;

    private HttpClient httpClientGatekeeper = null;
    private S3ServiceMulti s3ServiceMulti = null;

    /**
     * The files to upload to S3.
     */
    private File[] filesToUpload = null;

    /**
     * The list of file extensions accepted by the Uploader.
     */
    private ArrayList validFileExtensions = new ArrayList();

    /**
     * Uploader's properties.
     */
    private Jets3tProperties uploaderProperties = null;

    /**
     * Properties set in stand-alone application from the command line arguments.
     */
    private Properties standAloneArgumentProperties = null;

    private final ByteFormatter byteFormatter = new ByteFormatter();
    private final TimeFormatter timeFormatter = new TimeFormatter();

    /*
     * Upload file constraints.
     */
    private int fileMaxCount = 0;
    private long fileMaxSizeMB = 0;
    private long fileMinSizeMB = 0;

    /*
     * Insets used throughout the application.
     */
    private final Insets insetsDefault = new Insets(3, 5, 3, 5);
    private final Insets insetsNone = new Insets(0, 0, 0, 0);

    private final GuiUtils guiUtils = new GuiUtils();

    private int currentState = 0;

    private boolean isRunningAsApplet = false;

    private HashMap parametersMap = new HashMap();

    private SkinsFactory skinsFactory = null;
    private final GridBagLayout GRID_BAG_LAYOUT = new GridBagLayout();

    /*
     * GUI elements that need to be referenced outside initGui method.
     */
    private JHtmlLabel userGuidanceLabel = null;
    private JPanel appContentPanel = null;
    private JPanel buttonsPanel = null;
    private JPanel primaryPanel = null;
    private CardLayout primaryPanelCardLayout = null;
    private CardLayout buttonsPanelCardLayout = null;
    private JButton backButton = null;
    private JButton nextButton = null;
    private JButton cancelUploadButton = null;
    private JHtmlLabel dragDropTargetLabel = null;
    private JHtmlLabel fileToUploadLabel = null;
    private JHtmlLabel fileInformationLabel = null;
    private JHtmlLabel progressTransferDetailsLabel = null;
    private JProgressBar progressBar = null;
    private JHtmlLabel progressStatusTextLabel = null;
    private JHtmlLabel finalMessageLabel = null;
    private CancelEventTrigger uploadCancelEventTrigger = null;

    /**
     * Set to true when the object/file being uploaded is the final in a set, eg when
     * the XML metadata is being uploaded after a movie file.
     */
    private volatile boolean uploadingFinalObject = false;

    /**
     * If set to true via the "xmlSummary" configuration property, the Uploader will
     * generate an XML summary document describing the files uploaded by the user and
     * will upload this summary document to S3.
     */
    private volatile boolean includeXmlSummaryDoc = false;

    /**
     * Set to true when a file upload has been cancelled, to prevent the Uploader from
     * uploading an XML summary file when the prior file upload was cancelled.
     */
    private volatile boolean uploadCancelled = false;

    /**
     * Set to true if an upload failed due to a key name clash in S3, in which case an error
     * message is displayed in the final 'thankyou' screen.
     */
    private boolean fatalErrorOccurred = false;

    /**
     * Variable to store application exceptions, so that client failure information can be
     * included in the information provided to the Gatekeeper when uploads are retried.
     */
    private Exception priorFailureException = null;

    /**
     * Constructor to run this application as an Applet.
     */
    public Uploader() {
        isRunningAsApplet = true;
    }

    /**
     * Constructor to run this application in a stand-alone window.
     *
     * @param ownerFrame the frame the application will be displayed in
     * @throws S3ServiceException
     */
    public Uploader(JFrame ownerFrame, Properties standAloneArgumentProperties) throws S3ServiceException {
        this.ownerFrame = ownerFrame;
        this.standAloneArgumentProperties = standAloneArgumentProperties;
        isRunningAsApplet = false;

        init();

        ownerFrame.getContentPane().add(this);
        ownerFrame.setBounds(this.getBounds());
        ownerFrame.setVisible(true);
    }

    /**
     * Prepares application to run as a GUI by finding/creating a root owner JFrame, and
     * (if necessary) creating a directory for storing remembered logins.
     */
    @Override
    public void init() {
        super.init();

        boolean isMissingRequiredInitProperty = false;

        // Find or create a Frame to own modal dialog boxes.
        if (this.ownerFrame == null) {
            Component c = this;
            while (!(c instanceof Frame) && c.getParent() != null) {
                c = c.getParent();
            }
            if (!(c instanceof Frame)) {
                this.ownerFrame = new JFrame();
            } else {
                this.ownerFrame = (Frame) c;
            }
        }

        // Read properties from uploader.properties in classpath.
        uploaderProperties = Jets3tProperties.getInstance(UPLOADER_PROPERTIES_FILENAME);

        if (isRunningAsApplet) {
            // Read parameters for Applet, based on names specified in the uploader properties.
            String appletParamNames = uploaderProperties.getStringProperty("applet.params", "");
            StringTokenizer st = new StringTokenizer(appletParamNames, ",");
            while (st.hasMoreTokens()) {
                String paramName = st.nextToken();
                String paramValue = this.getParameter(paramName);

                // Fatal error if a parameter is missing.
                if (null == paramValue) {
                    log.error("Missing required applet parameter: " + paramName);
                    isMissingRequiredInitProperty = true;
                } else {
                    log.debug("Found applet parameter: " + paramName + "='" + paramValue + "'");

                    // Set params as properties in the central properties source for this application.
                    // Note that parameter values will over-write properties with the same name.
                    uploaderProperties.setProperty(paramName, paramValue);

                    // Store params in a separate map, which is used to build XML document.
                    parametersMap.put(paramName, paramValue);
                }
            }
        } else {
            // Add application parameters properties.
            if (standAloneArgumentProperties != null) {
                Enumeration e = standAloneArgumentProperties.keys();
                while (e.hasMoreElements()) {
                    String propName = (String) e.nextElement();
                    String propValue = standAloneArgumentProperties.getProperty(propName);

                    // Fatal error if a parameter is missing.
                    if (null == propValue) {
                        log.error("Missing required command-line property: " + propName);
                        isMissingRequiredInitProperty = true;
                    } else {
                        log.debug("Using command-line property: " + propName + "='" + propValue + "'");

                        // Set arguments as properties in the central properties source for this application.
                        // Note that argument values will over-write properties with the same name.
                        uploaderProperties.setProperty(propName, propValue);

                        // Store arguments in a separate map, which is used to build XML document.
                        parametersMap.put(propName, propValue);
                    }
                }
            }
        }

        // Determine the file constraints.
        fileMaxCount = uploaderProperties.getIntProperty("file.maxCount", 1);
        fileMaxSizeMB = uploaderProperties.getLongProperty("file.maxSizeMB", 200);
        fileMinSizeMB = uploaderProperties.getLongProperty("file.minSizeMB", 0);

        // Initialise the GUI.
        initGui();

        // Jump to error page if there was an exception raised during initialisation.
        if (isMissingRequiredInitProperty) {
            failWithFatalError(ERROR_CODE__MISSING_REQUIRED_PARAM);
            return;
        }

        // Determine valid file extensions.
        String validFileExtensionsStr = uploaderProperties.getStringProperty("file.extensions", "");
        if (validFileExtensionsStr != null) {
            StringTokenizer st = new StringTokenizer(validFileExtensionsStr, ",");
            while (st.hasMoreTokens()) {
                validFileExtensions.add(st.nextToken().toLowerCase(Locale.getDefault()));
            }
        }
    }


    /**
     * Initialises the application's GUI elements.
     */
    private void initGui() {
        // Initialise skins factory.
        skinsFactory = SkinsFactory.getInstance(uploaderProperties.getProperties());

        // Set Skinned Look and Feel.
        LookAndFeel lookAndFeel = skinsFactory.createSkinnedMetalTheme("SkinnedLookAndFeel");
        try {
            UIManager.setLookAndFeel(lookAndFeel);
        } catch (UnsupportedLookAndFeelException e) {
            log.error("Unable to set skinned LookAndFeel", e);
        }

        // Apply branding
        String applicationTitle = replaceMessageVariables(
            uploaderProperties.getStringProperty("gui.applicationTitle", null));
        if (applicationTitle != null) {
            ownerFrame.setTitle(applicationTitle);
        }
        String applicationIconPath = uploaderProperties.getStringProperty("gui.applicationIcon", null);
        if (!isRunningAsApplet && applicationIconPath != null) {
            guiUtils.applyIcon(ownerFrame, applicationIconPath);
        }
        String footerHtml = uploaderProperties.getStringProperty("gui.footerHtml", null);
        String footerIconPath = uploaderProperties.getStringProperty("gui.footerIcon", null);

        // Footer for branding
        boolean includeFooter = false;
        JHtmlLabel footerLabel = skinsFactory.createSkinnedJHtmlLabel("FooterLabel");
        footerLabel.setHyperlinkeActivatedListener(this);
        footerLabel.setHorizontalAlignment(JLabel.CENTER);
        if (footerHtml != null) {
            footerLabel.setText(replaceMessageVariables(footerHtml));
            includeFooter = true;
        }
        if (footerIconPath != null) {
            guiUtils.applyIcon(footerLabel, footerIconPath);
        }

        userInputFields = new UserInputFields(insetsDefault, this, skinsFactory);

        // Screeen 1 : User input fields.
        JPanel screen1Panel = skinsFactory.createSkinnedJPanel("Screen1Panel");
        screen1Panel.setLayout(GRID_BAG_LAYOUT);
        userInputFields.buildFieldsPanel(screen1Panel, uploaderProperties);


        // Screen 2 : Drag/drop panel.
        JPanel screen2Panel = skinsFactory.createSkinnedJPanel("Screen2Panel");
        screen2Panel.setLayout(GRID_BAG_LAYOUT);
        dragDropTargetLabel = skinsFactory.createSkinnedJHtmlLabel("DragDropTargetLabel");
        dragDropTargetLabel.setHyperlinkeActivatedListener(this);
        dragDropTargetLabel.setHorizontalAlignment(JLabel.CENTER);
        dragDropTargetLabel.setVerticalAlignment(JLabel.CENTER);

        JButton chooseFileButton = skinsFactory.createSkinnedJButton("ChooseFileButton");
        chooseFileButton.setActionCommand("ChooseFile");
        chooseFileButton.addActionListener(this);
        configureButton(chooseFileButton, "screen.2.browseButton");

        screen2Panel.add(dragDropTargetLabel,
            new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsDefault, 0, 0));
        screen2Panel.add(chooseFileButton,
            new GridBagConstraints(0, 1, 1, 1, 1, 1, GridBagConstraints.NORTH, GridBagConstraints.NONE, insetsDefault, 0, 0));

        // Screen 3 : Information about the file to be uploaded.
        JPanel screen3Panel = skinsFactory.createSkinnedJPanel("Screen3Panel");
        screen3Panel.setLayout(GRID_BAG_LAYOUT);
        fileToUploadLabel = skinsFactory.createSkinnedJHtmlLabel("FileToUploadLabel");
        fileToUploadLabel.setHyperlinkeActivatedListener(this);
        fileToUploadLabel.setHorizontalAlignment(JLabel.CENTER);
        fileToUploadLabel.setVerticalAlignment(JLabel.CENTER);
        screen3Panel.add(fileToUploadLabel,
            new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsDefault, 0, 0));

        // Screen 4 : Upload progress.
        JPanel screen4Panel = skinsFactory.createSkinnedJPanel("Screen4Panel");
        screen4Panel.setLayout(GRID_BAG_LAYOUT);
        fileInformationLabel = skinsFactory.createSkinnedJHtmlLabel("FileInformationLabel");
        fileInformationLabel.setHyperlinkeActivatedListener(this);
        fileInformationLabel.setHorizontalAlignment(JLabel.CENTER);
        progressBar = skinsFactory.createSkinnedJProgressBar("ProgressBar", 0, 100);
        progressBar.setStringPainted(true);
        progressStatusTextLabel = skinsFactory.createSkinnedJHtmlLabel("ProgressStatusTextLabel");
        progressStatusTextLabel.setHyperlinkeActivatedListener(this);
        progressStatusTextLabel.setText(" ");
        progressStatusTextLabel.setHorizontalAlignment(JLabel.CENTER);
        progressTransferDetailsLabel = skinsFactory.createSkinnedJHtmlLabel("ProgressTransferDetailsLabel");
        progressTransferDetailsLabel.setHyperlinkeActivatedListener(this);
        progressTransferDetailsLabel.setText(" ");
        progressTransferDetailsLabel.setHorizontalAlignment(JLabel.CENTER);
        cancelUploadButton = skinsFactory.createSkinnedJButton("CancelUploadButton");
        cancelUploadButton.setActionCommand("CancelUpload");
        cancelUploadButton.addActionListener(this);
        configureButton(cancelUploadButton, "screen.4.cancelButton");

        screen4Panel.add(fileInformationLabel,
            new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, insetsDefault, 0, 0));
        screen4Panel.add(progressBar,
            new GridBagConstraints(0, 1, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, insetsDefault, 0, 0));
        screen4Panel.add(progressStatusTextLabel,
            new GridBagConstraints(0, 2, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, insetsDefault, 0, 0));
        screen4Panel.add(progressTransferDetailsLabel,
            new GridBagConstraints(0, 3, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, insetsDefault, 0, 0));
        screen4Panel.add(cancelUploadButton,
            new GridBagConstraints(0, 4, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, insetsDefault, 0, 0));

        // Screen 5 : Thankyou message.
        JPanel screen5Panel = skinsFactory.createSkinnedJPanel("Screen5Panel");
        screen5Panel.setLayout(GRID_BAG_LAYOUT);
        finalMessageLabel = skinsFactory.createSkinnedJHtmlLabel("FinalMessageLabel");
        finalMessageLabel.setHyperlinkeActivatedListener(this);
        finalMessageLabel.setHorizontalAlignment(JLabel.CENTER);
        screen5Panel.add(finalMessageLabel,
            new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsDefault, 0, 0));

        // Wizard Button panel.
        backButton = skinsFactory.createSkinnedJButton("backButton");
        backButton.setActionCommand("Back");
        backButton.addActionListener(this);
        nextButton = skinsFactory.createSkinnedJButton("nextButton");
        nextButton.setActionCommand("Next");
        nextButton.addActionListener(this);

        buttonsPanel = skinsFactory.createSkinnedJPanel("ButtonsPanel");
        buttonsPanelCardLayout = new CardLayout();
        buttonsPanel.setLayout(buttonsPanelCardLayout);
        JPanel buttonsInvisiblePanel = skinsFactory.createSkinnedJPanel("ButtonsInvisiblePanel");
        JPanel buttonsVisiblePanel = skinsFactory.createSkinnedJPanel("ButtonsVisiblePanel");
        buttonsVisiblePanel.setLayout(GRID_BAG_LAYOUT);
        buttonsVisiblePanel.add(backButton,
            new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, insetsDefault, 0, 0));
        buttonsVisiblePanel.add(nextButton,
            new GridBagConstraints(1, 0, 1, 1, 1, 0, GridBagConstraints.EAST, GridBagConstraints.NONE, insetsDefault, 0, 0));
        buttonsPanel.add(buttonsInvisiblePanel, "invisible");
        buttonsPanel.add(buttonsVisiblePanel, "visible");

        // Overall content panel.
        appContentPanel = skinsFactory.createSkinnedJPanel("ApplicationContentPanel");
        appContentPanel.setLayout(GRID_BAG_LAYOUT);
        JPanel userGuidancePanel = skinsFactory.createSkinnedJPanel("UserGuidancePanel");
        userGuidancePanel.setLayout(GRID_BAG_LAYOUT);
        primaryPanel = skinsFactory.createSkinnedJPanel("PrimaryPanel");
        primaryPanelCardLayout = new CardLayout();
        primaryPanel.setLayout(primaryPanelCardLayout);
        JPanel navigationPanel = skinsFactory.createSkinnedJPanel("NavigationPanel");
        navigationPanel.setLayout(GRID_BAG_LAYOUT);

        appContentPanel.add(userGuidancePanel,
            new GridBagConstraints(0, 0, 1, 1, 1, 0.2, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsDefault, 0, 0));
        appContentPanel.add(primaryPanel,
            new GridBagConstraints(0, 1, 1, 1, 1, 0.6, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsDefault, 0, 0));
        appContentPanel.add(navigationPanel,
            new GridBagConstraints(0, 2, 1, 1, 1, 0.2, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsDefault, 0, 0));
        if (includeFooter) {
            log.debug("Adding footer for branding");
            appContentPanel.add(footerLabel,
                new GridBagConstraints(0, 3, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, insetsDefault, 0, 0));
        }
        this.getContentPane().add(appContentPanel);

        userGuidanceLabel = skinsFactory.createSkinnedJHtmlLabel("UserGuidanceLabel");
        userGuidanceLabel.setHyperlinkeActivatedListener(this);
        userGuidanceLabel.setHorizontalAlignment(JLabel.CENTER);

        userGuidancePanel.add(userGuidanceLabel,
            new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insetsNone, 0, 0));
        navigationPanel.add(buttonsPanel,
            new GridBagConstraints(0, 0, 1, 1, 1, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, insetsNone, 0, 0));

        primaryPanel.add(screen1Panel, "screen1");
        primaryPanel.add(screen2Panel, "screen2");
        primaryPanel.add(screen3Panel, "screen3");
        primaryPanel.add(screen4Panel, "screen4");
        primaryPanel.add(screen5Panel, "screen5");

        // Set preferred sizes
        int preferredWidth = uploaderProperties.getIntProperty("gui.minSizeWidth", 400);
        int preferredHeight = uploaderProperties.getIntProperty("gui.minSizeHeight", 500);
        this.setBounds(new Rectangle(new Dimension(preferredWidth, preferredHeight)));

        // Initialize drop target.
        initDropTarget(new Component[] {this} );

        // Revert to default Look and Feel for all future GUI elements (eg Dialog boxes).
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
            log.error("Unable to set default system LookAndFeel", e);
        }

        wizardStepForward();
    }

    /**
     * Initialise the application's File drop targets for drag and drop copying of local files
     * to S3.
     *
     * @param dropTargetComponents
     * the components files can be dropped on to transfer them to S3
     */
    private void initDropTarget(Component[] dropTargetComponents) {
        DropTargetListener dropTargetListener = new DropTargetListener() {

            private Border originalBorder = appContentPanel.getBorder();
            private Border dragOverBorder = BorderFactory.createBevelBorder(1);

            private boolean checkValidDrag(DropTargetDragEvent dtde) {
                if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
                    && (DnDConstants.ACTION_COPY == dtde.getDropAction()
                        || DnDConstants.ACTION_MOVE == dtde.getDropAction()))
                {
                    dtde.acceptDrag(dtde.getDropAction());
                    return true;
                } else {
                    dtde.rejectDrag();
                    return false;
                }
            }

            public void dragEnter(DropTargetDragEvent dtde) {
                if (checkValidDrag(dtde)) {
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            appContentPanel.setBorder(dragOverBorder);
                        };
                    });
                }
            }
            public void dragOver(DropTargetDragEvent dtde) {
                checkValidDrag(dtde);
            }

            public void dropActionChanged(DropTargetDragEvent dtde) {
                checkValidDrag(dtde);
            }

            public void dragExit(DropTargetEvent dte) {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        appContentPanel.setBorder(originalBorder);
                    };
                });
            }

            public void drop(DropTargetDropEvent dtde) {
                if (dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
                    && (DnDConstants.ACTION_COPY == dtde.getDropAction()
                        || DnDConstants.ACTION_MOVE == dtde.getDropAction()))
                {
                    dtde.acceptDrop(dtde.getDropAction());
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            appContentPanel.setBorder(originalBorder);
                        };
                    });

                    try {
                        final List fileList = (List) dtde.getTransferable().getTransferData(
                            DataFlavor.javaFileListFlavor);
                        if (fileList != null && fileList.size() > 0) {
                            if (checkProposedUploadFiles(fileList)) {
                                wizardStepForward();
                            }
                        }
                    } catch (Exception e) {
                        String errorMessage = "Unable to accept dropped item";
                        log.error(errorMessage, e);
                        ErrorDialog.showDialog(ownerFrame, null, uploaderProperties.getProperties(),
                            errorMessage, e);
                    }
                } else {
                    dtde.rejectDrop();
                }
            }
        };

        // Attach drop target listener to each target component.
        for (int i = 0; i < dropTargetComponents.length; i++) {
            new DropTarget(dropTargetComponents[i], DnDConstants.ACTION_COPY, dropTargetListener, true);
        }
    }

    /**
     * Checks that all the files in a list are acceptable for uploading.
     * Files are checked against the following criteria:
     * <ul>
     * <li>There are not too many</li>
     * <li>The size is greater than the minimum size, and less that the maximum size.</li>
     * <li>The file has a file extension matching one of those explicitly allowed</li>
     * </ul>
     * Any deviations from the rules result in an error message, and the method returns false.
     * A side-effect of this method is that the wizard is moved forward one screen if the
     * files are all valid.
     *
     * @param fileList
     * A list of {@link File}s to check.
     *
     * @return
     * true if the files in the list are all acceptable, false otherwise.
     */
    private boolean checkProposedUploadFiles(List fileList) {
        // Check number of files for upload is within constraints.
        if (fileMaxCount > 0 && fileList.size() > fileMaxCount) {
            String errorMessage = "You may only upload " + fileMaxCount
                + (fileMaxCount == 1? " file" : " files") + " at a time";
            ErrorDialog.showDialog(ownerFrame, this, uploaderProperties.getProperties(),
                errorMessage, null);
            return false;
        }

        // Check file size within constraints.
        Iterator iter = fileList.iterator();
        while (iter.hasNext()) {
            File file = (File) iter.next();
            long fileSizeMB = file.length() / (1024 * 1024);

            if (fileMinSizeMB > 0 && fileSizeMB < fileMinSizeMB) {
                ErrorDialog.showDialog(ownerFrame, this, uploaderProperties.getProperties(),
                    "File size must be greater than " + fileMinSizeMB + " MB", null);
                return false;
            }
            if (fileMaxSizeMB > 0 && fileSizeMB > fileMaxSizeMB) {
                ErrorDialog.showDialog(ownerFrame, this, uploaderProperties.getProperties(),
                    "File size must be less than " + fileMaxSizeMB + " MB", null);
                return false;
            }
        }

        // Check file extension is acceptable.
        if (validFileExtensions.size() > 0) {
            iter = fileList.iterator();
            while (iter.hasNext()) {
                File file = (File) iter.next();
                String fileName = file.getName();
                String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);
                if (!validFileExtensions.contains(fileExtension.toLowerCase(Locale.getDefault()))) {
                    String extList = validFileExtensions.toString();
                    extList = extList.substring(1, extList.length() -1);
                    extList = extList.replaceAll(",", " ");

                    ErrorDialog.showDialog(ownerFrame, this, uploaderProperties.getProperties(),
                        "<html>File name must end with one of the following extensions:<br>" +
                        extList + "</html>", null);
                    return false;
                }
            }
        }

        filesToUpload = (File[]) fileList.toArray(new File[fileList.size()]);
        return true;
    }

    /**
     * Builds a Gatekeeper response based on AWS credential information available in the Uploader
     * properties. The response signs URLs to be valid for 1 day.
     * <p>
     * The required properties are:
     * <ul>
     * <li>AwsAccessKey</li>
     * <li>AwsSecretKey</li>
     * <li>S3BucketName</li>
     * </ul>
     *
     * @param objects
     * @return
     */
    private GatekeeperMessage buildGatekeeperResponse(S3Object[] objects) throws Exception {

        String awsAccessKey = userInputProperties.getProperty("AwsAccessKey");
        String awsSecretKey = userInputProperties.getProperty("AwsSecretKey");
        String s3BucketName = userInputProperties.getProperty("S3BucketName");

        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DATE, 1);
        Date expiryDate = cal.getTime();

        AWSCredentials awsCredentials = new AWSCredentials(awsAccessKey, awsSecretKey);
        RestS3Service s3Service = new RestS3Service(awsCredentials);

        try {
            /*
             *  Build Gatekeeper request.
             */
            GatekeeperMessage gatekeeperMessage = new GatekeeperMessage();
            gatekeeperMessage.addApplicationProperties(userInputProperties); // Add User inputs as application properties.
            gatekeeperMessage.addApplicationProperties(parametersMap); // Add any Applet/Application parameters as application properties.

            for (int i = 0; i < objects.length; i++) {
                String signedPutUrl = s3Service.createSignedPutUrl(
                    s3BucketName, objects[i].getKey(), objects[i].getMetadataMap(),
                    expiryDate, false);

                SignatureRequest signatureRequest = new SignatureRequest(
                    SignatureRequest.SIGNATURE_TYPE_PUT, objects[i].getKey());
                signatureRequest.setBucketName(s3BucketName);
                signatureRequest.setObjectMetadata(objects[i].getMetadataMap());
                signatureRequest.signRequest(signedPutUrl);

                gatekeeperMessage.addSignatureRequest(signatureRequest);
            }

            return gatekeeperMessage;

        } catch (Exception e) {
            throw new Exception("Unable to generate locally-signed PUT URLs for testing", e);
        }
    }

    /**
     * Retrieves a signed PUT URL from the given URL address.
     * The URL must point at a server-side script or service that accepts POST messages.
     * The POST message will include parameters for all the items in uploaderProperties,
     * that is everything in the file uploader.properties plus all the applet's parameters.
     * Based on this input, the server-side script decides whether to allow access and return
     * a signed PUT URL.
     *
     * @param credsProviderParamName
     * the name of the parameter containing the server URL target for the PUT request.
     * @return
     * the AWS credentials provided by the server-side script if access was allowed, null otherwise.
     *
     * @throws HttpException
     * @throws Exception
     */
    private GatekeeperMessage contactGatewayServer(S3Object[] objects)
        throws HttpException, Exception
    {
        // Retrieve credentials from URL location value by the property 'credentialsServiceUrl'.
        String gatekeeperUrl = uploaderProperties.getStringProperty(
            "gatekeeperUrl", "Missing required property gatekeeperUrl");

        /*
         *  Build Gatekeeper request.
         */
        GatekeeperMessage gatekeeperMessage = new GatekeeperMessage();
        gatekeeperMessage.addApplicationProperties(userInputProperties); // Add User inputs as application properties.
        gatekeeperMessage.addApplicationProperties(parametersMap); // Add any Applet/Application parameters as application properties.

        // Make the Uploader's identifier available to Gatekeeper for version compatibility checking (if necessary)
        gatekeeperMessage.addApplicationProperty(
            GatekeeperMessage.PROPERTY_CLIENT_VERSION_ID, UPLOADER_VERSION_ID);

        // If a prior failure has occurred, add information about this failure.
        if (priorFailureException != null) {
            gatekeeperMessage.addApplicationProperty(GatekeeperMessage.PROPERTY_PRIOR_FAILURE_MESSAGE,
                priorFailureException.getMessage());
            // Now reset the prior failure variable.
            priorFailureException = null;
        }

        // Add all S3 objects as candiates for PUT signing.
        for (int i = 0; i < objects.length; i++) {
            SignatureRequest signatureRequest = new SignatureRequest(
                SignatureRequest.SIGNATURE_TYPE_PUT, objects[i].getKey());
            signatureRequest.setObjectMetadata(objects[i].getMetadataMap());

            gatekeeperMessage.addSignatureRequest(signatureRequest);
        }


        /*
         *  Build HttpClient POST message.
         */

        // Add all properties/parameters to credentials POST request.
        PostMethod postMethod = new PostMethod(gatekeeperUrl);
        Properties properties = gatekeeperMessage.encodeToProperties();
        Iterator propsIter = properties.entrySet().iterator();
        while (propsIter.hasNext()) {
            Map.Entry entry = (Map.Entry) propsIter.next();
            String fieldName = (String) entry.getKey();
            String fieldValue = (String) entry.getValue();
            postMethod.setParameter(fieldName, fieldValue);
        }

        // Create Http Client if necessary, and include User Agent information.
        if (httpClientGatekeeper == null) {
            httpClientGatekeeper = initHttpConnection();
        }

        // Try to detect any necessary proxy configurations.
        try {
            ProxyHost proxyHost = PluginProxyUtil.detectProxy(new URL(gatekeeperUrl));
            if (proxyHost != null) {
                HostConfiguration hostConfig = new HostConfiguration();
                hostConfig.setProxyHost(proxyHost);
                httpClientGatekeeper.setHostConfiguration(hostConfig);

                httpClientGatekeeper.getParams().setAuthenticationPreemptive(true);
                httpClientGatekeeper.getParams().setParameter(CredentialsProvider.PROVIDER, this);
            }
        } catch (Throwable t) {
            log.debug("No proxy detected");
        }

        // Perform Gateway request.
        log.debug("Contacting Gatekeeper at: " + gatekeeperUrl);
        try {
            int responseCode = httpClientGatekeeper.executeMethod(postMethod);
            String contentType = postMethod.getResponseHeader("Content-Type").getValue();
            if (responseCode == 200) {
                InputStream responseInputStream = null;

                Header encodingHeader = postMethod.getResponseHeader("Content-Encoding");
                if (encodingHeader != null && "gzip".equalsIgnoreCase(encodingHeader.getValue())) {
                    log.debug("Inflating gzip-encoded response");
                    responseInputStream = new GZIPInputStream(postMethod.getResponseBodyAsStream());
                } else {
                    responseInputStream = postMethod.getResponseBodyAsStream();
                }

                if (responseInputStream == null) {
                    throw new IOException("No response input stream available from Gatekeeper");
                }

                Properties responseProperties = new Properties();
                try {
                    responseProperties.load(responseInputStream);
                } finally {
                    responseInputStream.close();
                }

                GatekeeperMessage gatekeeperResponseMessage =
                    GatekeeperMessage.decodeFromProperties(responseProperties);

                // Check for Gatekeeper Error Code in response.
                String gatekeeperErrorCode = gatekeeperResponseMessage.getApplicationProperties()
                    .getProperty(GatekeeperMessage.APP_PROPERTY_GATEKEEPER_ERROR_CODE);
                if (gatekeeperErrorCode != null) {
                    log.warn("Received Gatekeeper error code: " + gatekeeperErrorCode);
                    failWithFatalError(gatekeeperErrorCode);
                    return null;
                }

                if (gatekeeperResponseMessage.getSignatureRequests().length != objects.length) {
                    throw new Exception("The Gatekeeper service did not provide the necessary "
                        + objects.length + " response items");
                }

                return gatekeeperResponseMessage;
            } else {
                log.debug("The Gatekeeper did not permit a request. Response code: "
                    + responseCode + ", Response content type: " + contentType);
                throw new Exception("The Gatekeeper did not permit your request");
            }
        } catch (Exception e) {
            throw new Exception("Gatekeeper did not respond", e);
        } finally {
            postMethod.releaseConnection();
        }
    }

    private GatekeeperMessage retrieveGatekeeperResponse(S3Object[] objects) throws Exception {
        // Check whether Uploader has all necessary credentials from user inputs.
        boolean s3CredentialsProvided =
            userInputProperties.getProperty("AwsAccessKey") != null
            && userInputProperties.getProperty("AwsSecretKey") != null
            && userInputProperties.getProperty("S3BucketName") != null;

        GatekeeperMessage gatekeeperMessage = null;
        if (s3CredentialsProvided) {
            log.debug("S3 login credentials and bucket name are available, the Uploader "
                + "will generate its own Gatekeeper response");
            gatekeeperMessage = buildGatekeeperResponse(objects);
        } else {
            gatekeeperMessage = contactGatewayServer(objects);
        }
        return gatekeeperMessage;
    }

    /**
     * Uploads to S3 the file referenced by the variable fileToUpload, providing
     * progress feedback to the user all the while.
     *
     */
    private void uploadFilesToS3() {
        try {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    // Creating file hash message
                    progressStatusTextLabel.setText(replaceMessageVariables(
                        uploaderProperties.getStringProperty("screen.4.hashingMessage",
                        "Missing property 'screen.4.hashingMessage'")));
                };
            });

            // Calculate total files size.
            final long filesSizeTotal[] = new long[1];
            for (int i = 0; i < filesToUpload.length; i++) {
                filesSizeTotal[0] += filesToUpload[i].length();
            }

            // Monitor generation of MD5 hash, and provide feedback via the progress bar.
            BytesProgressWatcher progressWatcher = new BytesProgressWatcher(filesSizeTotal[0]) {
                @Override
                public void updateBytesTransferred(long byteCount) {
                    super.updateBytesTransferred(byteCount);

                    final int percentage =
                        (int)((double)getBytesTransferred() * 100 / getBytesToTransfer());
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            progressBar.setValue(percentage);
                        }
                    });
                }
            };

            // Create objects for upload from file listing.
            S3Object[] objectsForUpload = new S3Object[filesToUpload.length];
            for (int i = 0; i < filesToUpload.length; i++) {
                File file = filesToUpload[i];
                log.debug("Computing MD5 hash for file: " + file);
                byte[] fileHash = ServiceUtils.computeMD5Hash(
                    new ProgressMonitoredInputStream( // Report on MD5 hash progress.
                        new FileInputStream(file), progressWatcher));

                S3Object object = new S3Object(null, file);
                object.setMd5Hash(fileHash);
                objectsForUpload[i] = object;
            }

            // Obtain Gatekeeper response.
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    progressStatusTextLabel.setText(
                        replaceMessageVariables(uploaderProperties.getStringProperty("screen.4.connectingMessage",
                        "Missing property 'screen.4.connectingMessage'")));
                    progressBar.setValue(0);
                };
            });

            GatekeeperMessage gatekeeperMessage = null;

            try {
                gatekeeperMessage = retrieveGatekeeperResponse(objectsForUpload);
            } catch (Exception e) {
                log.info("Upload request was denied", e);
                failWithFatalError(ERROR_CODE__UPLOAD_REQUEST_DECLINED);
                return;
            }
            // If we get a null response, presume the error has already been handled.
            if (gatekeeperMessage == null) {
                return;
            }

            log.debug("Gatekeeper response properties: " + gatekeeperMessage.encodeToProperties());

            XmlGenerator xmlGenerator = new XmlGenerator();
            xmlGenerator.addApplicationProperties(gatekeeperMessage.getApplicationProperties());
            xmlGenerator.addMessageProperties(gatekeeperMessage.getMessageProperties());

            SignedUrlAndObject[] uploadItems = prepareSignedObjects(
                objectsForUpload, gatekeeperMessage.getSignatureRequests(), xmlGenerator);

            if (s3ServiceMulti == null) {
                s3ServiceMulti = new S3ServiceMulti(
                    new RestS3Service(null, APPLICATION_DESCRIPTION, this), this);
            }

            /*
             * Prepare XML Summary document for upload, if the summary option is set.
             */
            includeXmlSummaryDoc = uploaderProperties.getBoolProperty("xmlSummary", false);
            S3Object summaryXmlObject = null;
            if (includeXmlSummaryDoc) {
                String priorTransactionId = gatekeeperMessage.getMessageProperties().getProperty(
                    GatekeeperMessage.PROPERTY_TRANSACTION_ID);
                if (priorTransactionId == null) {
                    failWithFatalError(ERROR_CODE__TRANSACTION_ID_REQUIRED_TO_CREATE_XML_SUMMARY);
                    return;
                }

                summaryXmlObject = new S3Object(
                    null, priorTransactionId + ".xml", xmlGenerator.generateXml());
                summaryXmlObject.setContentType(Mimetypes.MIMETYPE_XML);
                summaryXmlObject.addMetadata(GatekeeperMessage.PROPERTY_TRANSACTION_ID, priorTransactionId);
                summaryXmlObject.addMetadata(GatekeeperMessage.SUMMARY_DOCUMENT_METADATA_FLAG, "true");
            }

            // PUT the user's selected files in S3.
            uploadCancelled = false;
            uploadingFinalObject = (!includeXmlSummaryDoc);
            s3ServiceMulti.putObjects(uploadItems);

            // If an XML summary document is required, PUT this in S3 as well.
            if (includeXmlSummaryDoc && !uploadCancelled && !fatalErrorOccurred) {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        fileInformationLabel.setText(
                            replaceMessageVariables(uploaderProperties.getStringProperty("screen.4.summaryFileInformation",
                            "Missing property 'screen.4.summaryFileInformation'")));
                        progressStatusTextLabel.setText(
                            replaceMessageVariables(uploaderProperties.getStringProperty("screen.4.connectingMessage",
                            "Missing property 'screen.4.connectingMessage'")));
                    };
                });

                // Retrieve signed URL to PUT the XML summary document.
                gatekeeperMessage = retrieveGatekeeperResponse(new S3Object[] {summaryXmlObject});
                SignedUrlAndObject[] xmlSummaryItem =
                    prepareSignedObjects(new S3Object[] {summaryXmlObject},
                        gatekeeperMessage.getSignatureRequests(), null);

                // PUT the XML summary document.
                uploadingFinalObject = true;
                s3ServiceMulti.putObjects(xmlSummaryItem);
            }
        } catch (final Exception e) {
            priorFailureException = e;

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    wizardStepBackward();
                    log.error("File upload failed", e);
                    ErrorDialog.showDialog(ownerFrame, null, uploaderProperties.getProperties(),
                        "File upload failed", e);
                };
            });
        }
    }

    private SignedUrlAndObject[] prepareSignedObjects(S3Object[] objects,
        SignatureRequest[] signatureRequests, XmlGenerator xmlGenerator) throws Exception
    {
        List signedObjects = new ArrayList();
        String firstDeclineReason = null;

        for (int i = 0; i < signatureRequests.length; i++) {
            SignatureRequest request = signatureRequests[i];
            S3Object object = objects[i];

            // Store summary information in XML document generator.
            if (xmlGenerator != null) {
                Map clonedMetadata = new HashMap();
                clonedMetadata.putAll(object.getMetadataMap());
                xmlGenerator.addSignatureRequest(object.getKey(), object.getBucketName(),
                    clonedMetadata, request);
            }

            if (request.isSigned()) {
                // Update object with any changes dictated by Gatekeeper.
                if (request.getObjectKey() != null) {
                    object.setKey(request.getObjectKey());
                }
                if (request.getBucketName() != null) {
                    object.setBucketName(request.getBucketName());
                }
                if (request.getObjectMetadata() != null && request.getObjectMetadata().size() > 0) {
                    object.replaceAllMetadata(request.getObjectMetadata());
                }

                SignedUrlAndObject urlAndObject = new SignedUrlAndObject(request.getSignedUrl(), object);
                signedObjects.add(urlAndObject);
            } else {
                // If ANY requests are declined, we will fail with a fatal error message.
                String declineReason = (request.getDeclineReason() == null
                    ? "Unknown"
                    : request.getDeclineReason());
                log.warn("Upload of '" + objects[i].getKey() + "' was declined for reason: "
                    + declineReason);
                if (firstDeclineReason == null) {
                    firstDeclineReason = declineReason;
                }
            }
        }
        if (firstDeclineReason != null) {
            throw new Exception("Your upload" + (objects.length > 1 ? "s were" : " was")
                + " declined by the Gatekeeper. Reason: " + firstDeclineReason);
        }
        return (SignedUrlAndObject[]) signedObjects.toArray(new SignedUrlAndObject[signedObjects.size()]);
    }

    /**
     * Listener method that responds to events from the jets3t toolkit when objects are
     * created in S3 - ie when files are uploaded.
     */
    public void s3ServiceEventPerformed(final CreateObjectsEvent event) {
        if (ServiceEvent.EVENT_STARTED == event.getEventCode()) {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    // Cancel button is enabled unless this upload is for the XML summary doc.
                    boolean isXmlSummaryUpload = includeXmlSummaryDoc && uploadingFinalObject;
                    cancelUploadButton.setEnabled(!isXmlSummaryUpload);
                }
            });

            ThreadWatcher watcher = event.getThreadWatcher();
            uploadCancelEventTrigger = watcher.getCancelEventListener();

            // Show percentage of bytes transferred.
            String bytesTotalStr = byteFormatter.formatByteSize(watcher.getBytesTotal());
            final String statusText = "Uploaded 0 of " + bytesTotalStr;

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    progressStatusTextLabel.setText(replaceMessageVariables(statusText));
                    progressBar.setValue(0);
                }
            });
        }
        else if (ServiceEvent.EVENT_IN_PROGRESS == event.getEventCode()) {
            ThreadWatcher watcher = event.getThreadWatcher();

            if (watcher.getBytesTransferred() >= watcher.getBytesTotal()) {
                // Upload is completed, just waiting on resonse from S3.
                String statusText = "Upload completed, awaiting confirmation";

                progressBar.setValue(100);
                progressStatusTextLabel.setText(replaceMessageVariables(statusText));
                progressTransferDetailsLabel.setText("");
            } else {
                String bytesCompletedStr = byteFormatter.formatByteSize(watcher.getBytesTransferred());
                String bytesTotalStr = byteFormatter.formatByteSize(watcher.getBytesTotal());
                String statusText = "Uploaded " + bytesCompletedStr + " of " + bytesTotalStr;
                int percentage = (int)
                    (((double)watcher.getBytesTransferred() / watcher.getBytesTotal()) * 100);

                long bytesPerSecond = watcher.getBytesPerSecond();
                String transferDetailsText = "Speed: " + byteFormatter.formatByteSize(bytesPerSecond) + "/s";

                if (watcher.isTimeRemainingAvailable()) {
                    long secondsRemaining = watcher.getTimeRemaining();
                    if (transferDetailsText.trim().length() > 0) {
                        transferDetailsText += " - ";
                    }
                    transferDetailsText += "Time remaining: " + timeFormatter.formatTime(secondsRemaining);
                }

                progressBar.setValue(percentage);
                progressStatusTextLabel.setText(replaceMessageVariables(statusText));
                progressTransferDetailsLabel.setText(replaceMessageVariables(transferDetailsText));
            }
        }
        else if (ServiceEvent.EVENT_COMPLETED == event.getEventCode()) {
            if (uploadingFinalObject) {
                drawWizardScreen(WIZARD_SCREEN_5);
            }
            progressBar.setValue(0);
            progressStatusTextLabel.setText("");
            progressTransferDetailsLabel.setText("");
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    cancelUploadButton.setEnabled(false);
                }
            });
        }
        else if (ServiceEvent.EVENT_CANCELLED == event.getEventCode()) {
            progressBar.setValue(0);
            progressStatusTextLabel.setText("");
            progressTransferDetailsLabel.setText("");
            uploadCancelled = true;
            drawWizardScreen(WIZARD_SCREEN_3);
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    cancelUploadButton.setEnabled(false);
                }
            });
        }
        else if (ServiceEvent.EVENT_ERROR == event.getEventCode()) {
            progressBar.setValue(0);
            progressStatusTextLabel.setText("");
            progressTransferDetailsLabel.setText("");
            failWithFatalError(ERROR_CODE__S3_UPLOAD_FAILED);
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    cancelUploadButton.setEnabled(false);
                }
            });
        }
    }

    /**
     * Configures a button's text, tooltip and image using uploader properties prefixed
     * with the given properties prefix.
     *
     * @param button
     * @param propertiesPrefix
     */
    private void configureButton(JButton button, String propertiesPrefix) {
        button.setHorizontalAlignment(JLabel.CENTER);

        String buttonImagePath = uploaderProperties
            .getStringProperty(propertiesPrefix + ".image", null);
        String buttonText = replaceMessageVariables(uploaderProperties
            .getStringProperty(propertiesPrefix + ".text", null));
        String buttonTooltip = replaceMessageVariables(uploaderProperties
            .getStringProperty(propertiesPrefix + ".tooltip", null));

        boolean hasImage = false;
        boolean hasText = false;

        if (buttonImagePath != null && buttonImagePath.length() > 0) {
            if (!guiUtils.applyIcon(button, buttonImagePath)) {
                log.error("Unable to load image URL for a button with property prefix '"
                    + propertiesPrefix + "'. Image path: " + buttonImagePath);
            } else {
                hasImage = true;
            }
        }
        if (buttonText != null && buttonText.length() > 0) {
            String text = replaceMessageVariables(buttonText);
            button.setText(text);
            button.setMnemonic(text.charAt(0));
            hasText = true;
        }
        if (buttonTooltip != null && buttonTooltip.length() > 0) {
            button.setToolTipText(buttonTooltip);
        }

        if (!hasImage && !hasText) {
            button.setVisible(false);
        } else {
            button.setVisible(true);
        }
    }

    /**
     * Draws the wizard screen appropriate to the stage in the wizard process the user has
     * reached.
     *
     * @param nextState
     * an integer detailing the screen the user is moving to.
     */
    private void drawWizardScreen(int nextState) {
        // Configure screen based on properties.
        String title = uploaderProperties.getStringProperty(
            "screen." + nextState + ".title", "");
        userGuidanceLabel.setText(replaceMessageVariables(title));

        configureButton(nextButton, "screen." + nextState + ".nextButton");
        configureButton(backButton, "screen." + nextState + ".backButton");

        this.getDropTarget().setActive(false);

        if (nextState == WIZARD_SCREEN_1) {
            primaryPanelCardLayout.show(primaryPanel, "screen1");
            buttonsPanelCardLayout.show(buttonsPanel, "visible");
        } else if (nextState == WIZARD_SCREEN_2) {
            userInputProperties = userInputFields.getUserInputsAsProperties(false);

            primaryPanelCardLayout.show(primaryPanel, "screen2");
            dragDropTargetLabel.setText(
                replaceMessageVariables(uploaderProperties.getStringProperty("screen.2.dragDropPrompt",
                "Missing property 'screen.2.dragDropPrompt'")));
            this.getDropTarget().setActive(true);
        } else if (nextState == WIZARD_SCREEN_3) {
            primaryPanelCardLayout.show(primaryPanel, "screen3");
            String fileInformation = uploaderProperties.getStringProperty("screen.3.fileInformation",
                "Missing property 'screen.3.fileInformation'");
            fileToUploadLabel.setText(replaceMessageVariables(fileInformation));
        } else if (nextState == WIZARD_SCREEN_4) {
            primaryPanelCardLayout.show(primaryPanel, "screen4");

            String fileInformation = uploaderProperties.getStringProperty("screen.4.fileInformation",
                "Missing property 'screen.4.fileInformation'");
            fileInformationLabel.setText(replaceMessageVariables(fileInformation));

            cancelUploadButton.setEnabled(false);
            new Thread() {
                @Override
                public void run() {
                    uploadFilesToS3();
                }
            }.start();
        } else if (nextState == WIZARD_SCREEN_5) {
            primaryPanelCardLayout.show(primaryPanel, "screen5");

            String finalMessage = null;
            if (fatalErrorOccurred) {
                finalMessage = uploaderProperties.getStringProperty("screen.5.errorMessage",
                    "Missing property 'screen.5.errorMessage'");
            } else {
                finalMessage = uploaderProperties.getStringProperty("screen.5.thankyouMessage",
                    "Missing property 'screen.5.thankyouMessage'");
            }

            finalMessageLabel.setText(replaceMessageVariables(finalMessage));
        } else {
            log.error("Ignoring unexpected wizard screen number: " + nextState);
            return;
        }
        currentState = nextState;
    }

    /**
     * Move the wizard forward one step/screen.
     */
    private void wizardStepForward() {
        drawWizardScreen(currentState + 1);
    }

    /**
     * Move the wizard backward one step/screen.
     */
    private void wizardStepBackward() {
        drawWizardScreen(currentState - 1);
    }

    /**
     * When a fatal error occurs, go straight to last screen to display the error message
     * and make the error code available as a variable (<code>${errorCode}</code>) to be used
     * in the error message displayed to the user.
     * <p>
     * If there is an Uploader property <code>errorCodeMessage.&lt;code&gt;</code> corresponding
     * to this error code, the value of this property is made available as a variable
     * (<code>${errorMessage}</code>). If there is no such property available the
     * <code>${errorMessage}</code> variable will be an empty string.
     *
     *
     * @param errorCode
     * the error code, which may correspond with an error message in uploader.properties.
     */
    private void failWithFatalError(String errorCode) {
        uploaderProperties.setProperty("errorCode", errorCode);

        String errorCodeMessagePropertyName = "errorCodeMessage." + errorCode;
        String errorCodeMessage = uploaderProperties.getStringProperty(errorCodeMessagePropertyName, "");
        uploaderProperties.setProperty("errorMessage", errorCodeMessage);

        fatalErrorOccurred = true;
        drawWizardScreen(WIZARD_SCREEN_5);
    }

    /**
     * Replaces variables of the form ${variableName} in the input string with the value of that
     * variable name in the local uploaderProperties properties object, or with one of the
     * following special variables:
     * <ul>
     * <li>fileName : Name of file being uploaded</li>
     * <li>fileSize : Size of the file being uploaded, eg 1.04 MB</li>
     * <li>filePath : Absolute path of the file being uploaded</li>
     * <li>maxFileSize : The maxiumum allowed file size in MB</li>
     * <li>maxFileCount : The maximum number of files that may be uploaded</li>
     * <li>validFileExtensions : A list of the file extensions allowed</li>
     * </ul>
     * If the variable named in the input string is not available, or has no value, the variable
     * reference is left in the result.
     *
     * @param message
     * string that may have variables to replace
     * @return
     * the input string with any variable referenced replaced with the variable's value.
     */
    private String replaceMessageVariables(String message) {
        if (message == null) {
            return "";
        }

        String result = message;
        // Replace upload file variables, if an upload file has been chosen.
        if (filesToUpload != null) {
            long filesSize = 0;
            StringBuffer fileNameList = new StringBuffer();
            for (int i = 0; i < filesToUpload.length; i++) {
                filesSize += filesToUpload[i].length();
                fileNameList.append(filesToUpload[i].getName()).append(" ");
            }

            result = result.replaceAll("\\$\\{fileNameList\\}", fileNameList.toString());
            result = result.replaceAll("\\$\\{filesSize\\}", byteFormatter.formatByteSize(filesSize));
        }
        result = result.replaceAll("\\$\\{maxFileSize\\}", String.valueOf(fileMaxSizeMB));
        result = result.replaceAll("\\$\\{maxFileCount\\}", String.valueOf(fileMaxCount));

        String extList = validFileExtensions.toString();
        extList = extList.substring(1, extList.length() -1);
        extList = extList.replaceAll(",", " ");
        result = result.replaceAll("\\$\\{validFileExtensions\\}", extList);

        Pattern pattern = Pattern.compile("\\$\\{.+?\\}");
        Matcher matcher = pattern.matcher(result);
        int offset = 0;
        while (matcher.find(offset)) {
            String variable = matcher.group();
            String variableName = variable.substring(2, variable.length() - 1);

            String replacement = null;
            if (userInputProperties != null && userInputProperties.containsKey(variableName)) {
                log.debug("Replacing variable '" + variableName + "' with value from a user input field");
                replacement = userInputProperties.getProperty(variableName, null);
            } else if (parametersMap != null && parametersMap.containsKey(variableName)) {
                log.debug("Replacing variable '" + variableName + "' with value from Uploader's parameters");
                replacement = (String) parametersMap.get(variableName);
            } else if (uploaderProperties != null && uploaderProperties.containsKey(variableName)) {
                log.debug("Replacing variable '" + variableName + "' with value from uploader.properties file");
                replacement = uploaderProperties.getStringProperty(variableName, null);
            }

            if (replacement != null) {
                result = result.substring(0, matcher.start()) + replacement +
                result.substring(matcher.end());
                offset = matcher.start() + 1;
                matcher.reset(result);
            } else {
                offset = matcher.start() + 1;
            }
        }
        if (!result.equals(message)) {
            log.debug("Replaced variables in text: " + message + " => " + result);
        }
        return result;
    }

    /**
     * Handles GUI actions.
     */
    public void actionPerformed(ActionEvent actionEvent) {
        if ("Next".equals(actionEvent.getActionCommand())) {
            wizardStepForward();
        } else if ("Back".equals(actionEvent.getActionCommand())) {
            wizardStepBackward();
        } else if ("ChooseFile".equals(actionEvent.getActionCommand())) {
            JFileChooser fileChooser = new JFileChooser();

            if (validFileExtensions.size() > 0) {
                UploaderFileExtensionFilter filter = new UploaderFileExtensionFilter(
                    "Allowed files", validFileExtensions);
                fileChooser.setFileFilter(filter);
            }

            fileChooser.setMultiSelectionEnabled(fileMaxCount > 1);
            fileChooser.setDialogTitle("Choose file" + (fileMaxCount > 1 ? "s" : "") + " to upload");
            fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
            fileChooser.setApproveButtonText("Choose file" + (fileMaxCount > 1 ? "s" : ""));

            int returnVal = fileChooser.showOpenDialog(ownerFrame);
            if (returnVal != JFileChooser.APPROVE_OPTION) {
                    return;
            }

            List fileList = new ArrayList();
            if (fileChooser.getSelectedFiles().length > 0) {
                fileList.addAll(Arrays.asList(fileChooser.getSelectedFiles()));
            } else {
                fileList.add(fileChooser.getSelectedFile());
            }
            if (checkProposedUploadFiles(fileList)) {
                wizardStepForward();
            }
        } else if ("CancelUpload".equals(actionEvent.getActionCommand())) {
            if (uploadCancelEventTrigger != null) {
                uploadCancelEventTrigger.cancelTask(this);
                progressBar.setValue(0);
            } else {
                log.warn("Ignoring attempt to cancel file upload when cancel trigger is not available");
            }
        } else {
            log.warn("Unrecognised action command, ignoring: " + actionEvent.getActionCommand());
        }
    }

    private HttpClient initHttpConnection() {
        // Set client parameters.
        HttpClientParams clientParams = new HttpClientParams();

        clientParams.setParameter(HttpMethodParams.USER_AGENT,
            ServiceUtils.getUserAgentDescription(APPLICATION_DESCRIPTION));

        // Replace default error retry handler.
        clientParams.setParameter(HttpClientParams.RETRY_HANDLER, new HttpMethodRetryHandler() {
            public boolean retryMethod(HttpMethod httpMethod, IOException ioe, int executionCount) {
                if (executionCount > MAX_CONNECTION_RETRIES) {
                    log.error("Retried connection " + executionCount
                        + " times, which exceeds the maximum retry count of " + MAX_CONNECTION_RETRIES);
                    return false;
                }
                log.warn("Retrying request - attempt " + executionCount + " of " + MAX_CONNECTION_RETRIES);
                return true;
            }
        });


        // Set connection parameters.
        HttpConnectionManagerParams connectionParams = new HttpConnectionManagerParams();
        connectionParams.setConnectionTimeout(HTTP_CONNECTION_TIMEOUT);
        connectionParams.setSoTimeout(SOCKET_CONNECTION_TIMEOUT);
        connectionParams.setStaleCheckingEnabled(false);

        HttpClient httpClient = new HttpClient(clientParams);
        httpClient.getHttpConnectionManager().setParams(connectionParams);
        return httpClient;
    }

    /**
     * Follows hyperlinks clicked on by a user. This is achieved differently depending on whether
     * Cockpit is running as an applet or as a stand-alone application:
     * <ul>
     * <li>Application: Detects the default browser application for the user's system (using
     * <tt>BareBonesBrowserLaunch</tt>) and opens the link as a new window in that browser</li>
     * <li>Applet: Opens the link in the current browser using the applet's context</li>
     * </ul>
     *
     * @param url
     * the url to open
     * @param target
     * the target pane to open the url in, eg "_blank". This may be null.
     */
    public void followHyperlink(URL url, String target) {
        if (isRunningAsApplet) {
            if (target == null) {
                getAppletContext().showDocument(url);
            } else {
                getAppletContext().showDocument(url, target);
            }
        } else {
            BareBonesBrowserLaunch.openURL(url.toString());
        }
    }

    /**
     * Implementation method for the CredentialsProvider interface.
     * <p>
     * Based on sample code:
     * <a href="http://svn.apache.org/viewvc/jakarta/commons/proper/httpclient/trunk/src/examples/InteractiveAuthenticationExample.java?view=markup">InteractiveAuthenticationExample</a>
     *
     */
    public Credentials getCredentials(AuthScheme authscheme, String host, int port, boolean proxy) throws CredentialsNotAvailableException {
        if (authscheme == null) {
            return null;
        }
        try {
            Credentials credentials = null;

            if (authscheme instanceof NTLMScheme) {
                AuthenticationDialog pwDialog = new AuthenticationDialog(
                    ownerFrame, "Authentication Required",
                    "<html>Host <b>" + host + ":" + port + "</b> requires Windows authentication</html>", true);
                pwDialog.setVisible(true);
                if (pwDialog.getUser().length() > 0) {
                    credentials = new NTCredentials(pwDialog.getUser(), pwDialog.getPassword(),
                        host, pwDialog.getDomain());
                }
                pwDialog.dispose();
            } else
            if (authscheme instanceof RFC2617Scheme) {
                AuthenticationDialog pwDialog = new AuthenticationDialog(
                    ownerFrame, "Authentication Required",
                    "<html><center>Host <b>" + host + ":" + port + "</b>"
                    + " requires authentication for the realm:<br><b>" + authscheme.getRealm() + "</b></center></html>", false);
                pwDialog.setVisible(true);
                if (pwDialog.getUser().length() > 0) {
                    credentials = new UsernamePasswordCredentials(pwDialog.getUser(), pwDialog.getPassword());
                }
                pwDialog.dispose();
            } else {
                throw new CredentialsNotAvailableException("Unsupported authentication scheme: " +
                    authscheme.getSchemeName());
            }
            return credentials;
        } catch (IOException e) {
            throw new CredentialsNotAvailableException(e.getMessage(), e);
        }
    }


    // S3 Service events that are not used in this Uploader application.
    public void s3ServiceEventPerformed(ListObjectsEvent event) {}
    public void s3ServiceEventPerformed(CreateBucketsEvent event) {}
    public void s3ServiceEventPerformed(DeleteObjectsEvent event) {}
    public void s3ServiceEventPerformed(GetObjectsEvent event) {}
    public void s3ServiceEventPerformed(GetObjectHeadsEvent event) {}
    public void s3ServiceEventPerformed(LookupACLEvent event) {}
    public void s3ServiceEventPerformed(UpdateACLEvent event) {}
    public void s3ServiceEventPerformed(DownloadObjectsEvent event) {}
    public void s3ServiceEventPerformed(CopyObjectsEvent event) {}
    public void s3ServiceEventPerformed(DeleteVersionedObjectsEvent event) {}
    public void valueChanged(ListSelectionEvent arg0) {}


    /**
     * Run the Uploader as a stand-alone application.
     *
     * @param args
     * @throws Exception
     */
    public static void main(String args[]) throws Exception {
        JFrame ownerFrame = new JFrame("JetS3t Uploader");
        ownerFrame.addWindowListener(new WindowListener() {
            public void windowOpened(WindowEvent e) {
            }
            public void windowClosing(WindowEvent e) {
                e.getWindow().dispose();
            }
            public void windowClosed(WindowEvent e) {
            }
            public void windowIconified(WindowEvent e) {
            }
            public void windowDeiconified(WindowEvent e) {
            }
            public void windowActivated(WindowEvent e) {
            }
            public void windowDeactivated(WindowEvent e) {
            }
        });

        // Read arguments as properties of the form: <propertyName>'='<propertyValue>
        Properties argumentProperties = new Properties();
        if (args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                String arg = args[i];
                int delimIndex = arg.indexOf("=");
                if (delimIndex >= 0) {
                    String name = arg.substring(0, delimIndex);
                    String value = arg.substring(delimIndex + 1);
                    argumentProperties.put(name, value);
                } else {
                    System.out.println("Ignoring property argument with incorrect format: " + arg);
                }
            }
        }

        new Uploader(ownerFrame, argumentProperties);
    }

}
