org.talend.librariesmanager.ui.dialogs.ExternalModulesInstallDialogWithProgress.java Source code

Java tutorial

Introduction

Here is the source code for org.talend.librariesmanager.ui.dialogs.ExternalModulesInstallDialogWithProgress.java

Source

// ============================================================================
//
// Copyright (C) 2006-2016 Talend Inc. - www.talend.com
//
// This source code is available under agreement available at
// %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt
//
// You should have received a copy of the agreement
// along with this program; if not, write to Talend SA
// 9 rue Pages 92150 Suresnes, France
//
// ============================================================================
package org.talend.librariesmanager.ui.dialogs;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang.ArrayUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.operation.ModalContext;
import org.eclipse.jface.wizard.ProgressMonitorPart;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.talend.commons.exception.CommonExceptionHandler;
import org.talend.commons.exception.ExceptionHandler;
import org.talend.commons.utils.system.EclipseCommandLine;
import org.talend.core.model.general.ModuleNeeded;
import org.talend.core.model.general.ModuleToInstall;
import org.talend.librariesmanager.utils.DownloadModuleRunnableWithLicenseDialog;
import org.talend.librariesmanager.utils.RemoteModulesHelper;

/**
 * adds a progress bar to Modules dialog. Mostly inspired by org.eclipse.jface.wizard.WizardDialog.
 * 
 */
@SuppressWarnings("rawtypes")
public class ExternalModulesInstallDialogWithProgress extends ExternalModulesInstallDialog {

    private Button closeButton;

    private SelectionAdapter closeListener;

    private ProgressMonitorPart progressMonitorPart;

    private Cursor waitCursor;

    private static final String FOCUS_CONTROL = "focusControl"; //$NON-NLS-1$

    private boolean fork = true;

    /**
     * The time in milliseconds where the last job finished. 'Enter' key presses are ignored for the next
     * {@link #RESTORE_ENTER_DELAY} milliseconds.
     * <p>
     * The value <code>-1</code> indicates that the traverse listener needs to be installed.
     * </p>
     * 
     * @since 3.6
     */
    private long timeWhenLastJobFinished = -1;

    /**
     * A delay in milliseconds that reduces the risk that the user accidentally triggers a button by pressing the
     * 'Enter' key immediately after a job has finished.
     * 
     * @since 3.6
     */
    private static final int RESTORE_ENTER_DELAY = 500;

    // The number of long running operation executed from the dialog.
    private long activeRunningOperations = 0;

    /**
     * runnable launched whe the dialog is openned
     */
    private IRunnableWithProgress initialRunnable;

    /**
     * DOC sgandon ExternalModulesInstallDialogWithProgress constructor comment.
     * 
     * @param shell
     * @param text
     * @param title
     */
    public ExternalModulesInstallDialogWithProgress(Shell shell, String text, String title, int extraSheelStyle) {
        super(shell, text, title);
        setShellStyle(getShellStyle() | extraSheelStyle);
        // since VAJava can't initialize an instance var with an anonymous
        // class outside a constructor we do it here:
        closeListener = new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                closePressed();
            }
        };
    }

    /**
     * DOC sgandon ExternalModulesInstallDialogWithProgress constructor comment.
     * 
     * @param shell
     * @param text
     * @param title
     */
    public ExternalModulesInstallDialogWithProgress(Shell shell, String text, String title) {
        this(shell, text, title, 0);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.talend.librariesmanager.ui.dialogs.ExternalModulesInstallDialog#createDialogArea(org.eclipse.swt.widgets.
     * Composite)
     */
    @Override
    protected Control createDialogArea(Composite parent) {
        Composite composite = (Composite) super.createDialogArea(parent);
        // Insert a progress monitor
        GridLayout pmlayout = new GridLayout();
        pmlayout.numColumns = 1;
        progressMonitorPart = createProgressMonitorPart(composite, pmlayout);
        GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
        progressMonitorPart.setLayoutData(gridData);
        progressMonitorPart.setVisible(false);
        return composite;
    }

    /**
     * Create the progress monitor part in the receiver.
     * 
     * @param composite
     * @param pmlayout
     * @return ProgressMonitorPart
     */
    protected ProgressMonitorPart createProgressMonitorPart(Composite composite, GridLayout pmlayout) {
        return new ProgressMonitorPart(composite, pmlayout, true) {

            String currentTask = null;

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.jface.wizard.ProgressMonitorPart#setBlocked(org.eclipse.core.runtime.IStatus)
             */
            @Override
            public void setBlocked(IStatus reason) {
                super.setBlocked(reason);
                getBlockedHandler().showBlocked(getShell(), this, reason, currentTask);
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.jface.wizard.ProgressMonitorPart#clearBlocked()
             */
            @Override
            public void clearBlocked() {
                super.clearBlocked();
                getBlockedHandler().clearBlocked();
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.jface.wizard.ProgressMonitorPart#beginTask(java.lang.String, int)
             */
            @Override
            public void beginTask(String name, int totalWork) {
                super.beginTask(name, totalWork);
                currentTask = name;
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.jface.wizard.ProgressMonitorPart#setTaskName(java.lang.String)
             */
            @Override
            public void setTaskName(String name) {
                super.setTaskName(name);
                currentTask = name;
            }

            /*
             * (non-Javadoc)
             * 
             * @see org.eclipse.jface.wizard.ProgressMonitorPart#subTask(java.lang.String)
             */
            @Override
            public void subTask(String name) {
                super.subTask(name);
                // If we haven't got anything yet use this value for more
                // context
                if (currentTask == null) {
                    currentTask = name;
                }
            }
        };
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.dialogs.TrayDialog#close() can be called when the user click on close system button
     */
    @Override
    public boolean close() {
        getProgressMonitor().setCanceled(true);
        if (activeRunningOperations <= 0) {
            return super.close();
        } else {
            return false;
        }
    }

    /*
     * called when the close button is pressed
     */

    protected void closePressed() {
        getProgressMonitor().setCanceled(true);
        if (activeRunningOperations <= 0) {
            // Close the dialog. The check whether the dialog can be
            // closed or not is done in <code>okToClose</code>.
            // This ensures that the check is also evaluated when the user
            // presses the window's close button.
            setReturnCode(CANCEL);
            close();
        } else {
            closeButton.setEnabled(false);
        }
    }

    @Override
    protected Button getButton(int id) {
        if (id == IDialogConstants.CLOSE_ID) {
            return closeButton;
        }
        return super.getButton(id);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.dialogs.Dialog#buttonPressed(int)
     */
    @Override
    protected void buttonPressed(int buttonId) {
        if (IDialogConstants.CLOSE_ID == buttonId) {
            closePressed();
        } // else cancel button has a listener already
    }

    protected IProgressMonitor getProgressMonitor() {
        return progressMonitorPart;
    }

    /**
     * This implementation of IRunnableContext#run(boolean, boolean, IRunnableWithProgress) blocks until the runnable
     * has been run, regardless of the value of <code>fork</code>. It is recommended that <code>fork</code> is set to
     * true in most cases. If <code>fork</code> is set to <code>false</code>, the runnable will run in the UI thread and
     * it is the runnable's responsibility to call <code>Display.readAndDispatch()</code> to ensure UI responsiveness.
     * 
     * UI state is saved prior to executing the long-running operation and is restored after the long-running operation
     * completes executing. Any attempt to change the UI state of the wizard in the long-running operation will be
     * nullified when original UI state is restored.
     * 
     */
    public void run(IRunnableWithProgress runnable) throws InvocationTargetException, InterruptedException {
        // The operation can only be canceled if it is executed in a separate
        // thread.
        // Otherwise the UI is blocked anyway.
        Object state = null;
        if (activeRunningOperations == 0) {
            state = aboutToStart();
        }
        activeRunningOperations++;
        progressMonitorPart.attachToCancelComponent(null);// nasty hack to enable the cancel button
        try {
            ModalContext.run(runnable, fork, getProgressMonitor(), getShell().getDisplay());
        } finally {
            // explicitly invoke done() on our progress monitor so that its
            // label does not spill over to the next invocation, see bug 271530
            if (getProgressMonitor() != null) {
                getProgressMonitor().done();
            }
            // Stop if this is the last one
            if (state != null) {
                timeWhenLastJobFinished = System.currentTimeMillis();
                stopped(state);
            }
            activeRunningOperations--;
        }
    }

    /**
     * About to start a long running operation triggered through the wizard. Shows the progress monitor and disables the
     * wizard's buttons and controls.
     * 
     * @param enableCancelButton <code>true</code> if the Cancel button should be enabled, and <code>false</code> if it
     * should be disabled
     * @return the saved UI state
     */
    @SuppressWarnings("unchecked")
    private Object aboutToStart() {
        Map savedState = null;
        if (getShell() != null) {
            // Save focus control
            Control focusControl = getShell().getDisplay().getFocusControl();
            if (focusControl != null && focusControl.getShell() != getShell()) {
                focusControl = null;
            }

            // Set the busy cursor to all shells.
            Display d = getShell().getDisplay();
            waitCursor = new Cursor(d, SWT.CURSOR_WAIT);
            setDisplayCursor(waitCursor);

            // Deactivate shell
            savedState = saveUIState();
            if (focusControl != null) {
                savedState.put(FOCUS_CONTROL, focusControl);
            }
            progressMonitorPart.setVisible(true);

            // Install traverse listener once in order to implement 'Enter' and 'Space' key blocking
            if (timeWhenLastJobFinished == -1) {
                timeWhenLastJobFinished = 0;
                getShell().addTraverseListener(new TraverseListener() {

                    @Override
                    public void keyTraversed(TraverseEvent e) {
                        if (e.detail == SWT.TRAVERSE_RETURN
                                || (e.detail == SWT.TRAVERSE_MNEMONIC && e.keyCode == 32)) {
                            // We want to ignore the keystroke when we detect that it has been received within the
                            // delay period after the last operation has finished. This prevents the user from
                            // accidentally
                            // hitting "Enter" or "Space", intending to cancel an operation, but having it processed
                            // exactly
                            // when the operation finished, thus traversing the wizard. If there is another operation
                            // still
                            // running, the UI is locked anyway so we are not in this code. This listener should fire
                            // only
                            // after the UI state is restored (which by definition means all jobs are done.
                            // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=287887
                            if (timeWhenLastJobFinished != 0
                                    && System.currentTimeMillis() - timeWhenLastJobFinished < RESTORE_ENTER_DELAY) {
                                e.doit = false;
                                return;
                            }
                            timeWhenLastJobFinished = 0;
                        }
                    }
                });
            }
        }
        return savedState;
    }

    /**
     * Creates the Cancel button for this wizard dialog. Creates a standard (<code>SWT.PUSH</code>) button and registers
     * for its selection events. Note that the number of columns in the button bar composite is incremented. The Cancel
     * button is created specially to give it a removeable listener.
     * 
     * @param parent the parent button bar
     * @return the new Cancel button
     */
    private Button createCloseButton(Composite parent) {
        // increment the number of columns in the button bar
        ((GridLayout) parent.getLayout()).numColumns++;
        Button button = new Button(parent, SWT.PUSH);
        button.setText(IDialogConstants.CLOSE_LABEL);
        setButtonLayoutData(button);
        button.setFont(parent.getFont());
        button.setData(new Integer(IDialogConstants.CLOSE_ID));
        button.addSelectionListener(closeListener);
        return button;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.talend.librariesmanager.ui.dialogs.ExternalModulesInstallDialog#createButtonsForButtonBar(org.eclipse.swt
     * .widgets.Composite)
     */
    @Override
    protected void createButtonsForButtonBar(Composite parent) {
        closeButton = createCloseButton(parent);// make the cancel button the most left
        super.createButtonsForButtonBar(parent);
    }

    /**
     * Sets the given cursor for all shells currently active for this window's display.
     * 
     * @param c the cursor
     */
    private void setDisplayCursor(Cursor c) {
        Shell[] shells = getShell().getDisplay().getShells();
        for (Shell shell2 : shells) {
            shell2.setCursor(c);
        }
    }

    /**
     * Captures and returns the enabled/disabled state of the wizard dialog's buttons and the tree of controls for the
     * currently showing page. All these controls are disabled in the process, with the possible exception of the Cancel
     * button.
     * 
     * @param keepCancelEnabled <code>true</code> if the Cancel button should remain enabled, and <code>false</code> if
     * it should be disabled
     * @return a map containing the saved state suitable for restoring later with <code>restoreUIState</code>
     * @see #restoreUIState
     */
    private Map saveUIState() {
        Map savedState = new HashMap(10);
        saveEnableStateAndSet(closeButton, savedState, "close", true); //$NON-NLS-1$
        //        saveEnableStateAndSet(tableViewerCreator.getTable(), savedState, "table", false); //$NON-NLS-1$
        saveEnableStateAndSet(installAllBtn, savedState, "install", false); //$NON-NLS-1$
        return savedState;
    }

    /**
     * Saves the enabled/disabled state of the given control in the given map, which must be modifiable.
     * 
     * @param w the control, or <code>null</code> if none
     * @param h the map (key type: <code>String</code>, element type: <code>Boolean</code>)
     * @param key the key
     * @param enabled <code>true</code> to enable the control, and <code>false</code> to disable it
     * @see #restoreEnableState(Control, Map, String)
     */
    @SuppressWarnings("unchecked")
    private void saveEnableStateAndSet(Control w, Map h, String key, boolean enabled) {
        if (w != null) {
            h.put(key, w.getEnabled() ? Boolean.TRUE : Boolean.FALSE);
            w.setEnabled(enabled);
        }
    }

    /**
     * Restores the enabled/disabled state of the wizard dialog's buttons and the tree of controls for the currently
     * showing page.
     * 
     * @param state a map containing the saved state as returned by <code>saveUIState</code>
     * @see #saveUIState
     */
    private void restoreUIState(Map state) {
        restoreEnableState(closeButton, state, "close"); //$NON-NLS-1$
        restoreEnableState(tableViewerCreator.getTable(), state, "table"); //$NON-NLS-1$
        restoreEnableState(installAllBtn, state, "install"); //$NON-NLS-1$
    }

    /**
     * Restores the enabled/disabled state of the given control.
     * 
     * @param w the control
     * @param h the map (key type: <code>String</code>, element type: <code>Boolean</code>)
     * @param key the key
     * @see #saveEnableStateAndSet
     */
    private void restoreEnableState(Control w, Map h, String key) {
        if (w != null) {
            Boolean b = (Boolean) h.get(key);
            if (b != null) {
                w.setEnabled(b.booleanValue());
            }
        }
    }

    /**
     * A long running operation triggered through the wizard was stopped either by user input or by normal end. Hides
     * the progress monitor and restores the enable state wizard's buttons and controls.
     * 
     * @param savedState the saved UI state as returned by <code>aboutToStart</code>
     * @see #aboutToStart
     */
    private void stopped(Object savedState) {
        if (getShell() != null && !getShell().isDisposed()) {
            progressMonitorPart.setVisible(false);
            progressMonitorPart.removeFromCancelComponent(closeButton);
            Map state = (Map) savedState;
            restoreUIState(state);
            setDisplayCursor(null);
            waitCursor.dispose();
            waitCursor = null;
            Control focusControl = (Control) state.get(FOCUS_CONTROL);
            if (focusControl != null && !focusControl.isDisposed()) {
                focusControl.setFocus();
            }
        }
    }

    void setInitialRunnable(final IRunnableWithProgress initialRunnable) {
        this.initialRunnable = initialRunnable;
    }

    /**
     * called when the initial runnable is finished or when the dialog is openned with no initial runnable
     */
    protected void initialRunnableDone() {
        Display.getDefault().asyncExec(new Runnable() {

            @Override
            public void run() {
                tableViewerCreator.init(inputList);
                addInstallButtons();
                updateInstallModulesButtonState();
            }
        });
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.dialogs.Dialog#create()
     */
    @Override
    public void create() {
        super.create();
        startInitialRunnable();

    }

    @Override
    protected void addListeners() {

        installAllBtn.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent event) {

                List<ModuleToInstall> toInstall = getModulesToBeInstalled();

                installAllBtn.setEnabled(false);
                try {
                    run(new DownloadModuleRunnableWithLicenseDialog(toInstall, getShell()));
                    // close the dialog box when the download is done if it has not been canceled
                    if (!getProgressMonitor().isCanceled()) {
                        setReturnCode(CANCEL);
                        close();
                    }
                } catch (InvocationTargetException e) {
                    // an error occured when fetching the modules, so report it to the user.
                    ExceptionHandler.process(e);
                } catch (InterruptedException e) {
                    // the thread was interupted
                    ExceptionHandler.process(e);
                } finally {
                    if (!installAllBtn.isDisposed()) {
                        installAllBtn.setEnabled(true);
                    }
                }
            }
        });
    }

    /**
     * start initial runnable and block until is has finished. it then update the dialog window with the retreived data
     * by calling initialRunnableDone(). if the initialRunnable is null then the initialRunnableDone()
     */
    public void startInitialRunnable() {
        if (initialRunnable != null) {
            Display.getDefault().asyncExec(new Runnable() {

                @Override
                public void run() {
                    try {
                        ExternalModulesInstallDialogWithProgress.this.run(initialRunnable);
                        initialRunnableDone();
                    } catch (InvocationTargetException e) {
                        // an error occured when fetching the modules, so report it to the user.
                        ExceptionHandler.process(e);
                    } catch (InterruptedException e) {
                        // the thread was interupted
                        ExceptionHandler.process(e);
                    }
                }
            });
        } else {// modules data are already in cache so show the dialog as this
            initialRunnableDone();
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.talend.librariesmanager.ui.dialogs.ExternalModulesInstallDialog#launchIndividualDownload(java.util.concurrent
     * .atomic.AtomicInteger, org.talend.core.model.general.ModuleToInstall)
     */
    @Override
    protected void launchIndividualDownload(final AtomicInteger enabledButtonCount, ModuleToInstall data,
            final Button button) {
        button.setEnabled(false);
        enabledButtonCount.decrementAndGet();
        DownloadModuleRunnableWithLicenseDialog downloadModuleRunnable = new DownloadModuleRunnableWithLicenseDialog(
                Collections.singletonList(data), getShell());
        try {
            run(downloadModuleRunnable);
        } catch (InvocationTargetException e) {
            individualDownloadFailed(enabledButtonCount, button);
            // an error occured when fetching the modules, so report it to the user.
            ExceptionHandler.process(e);
        } catch (InterruptedException e) {
            individualDownloadFailed(enabledButtonCount, button);
            // the thread was interupted
            ExceptionHandler.process(e);
        } finally {// if button canceled then enable button
            if (getProgressMonitor().isCanceled()) {
                individualDownloadFailed(enabledButtonCount, button);
            } else {// keep button disabled and make download all button disabled if that was the last
                Display.getDefault().syncExec(new Runnable() {

                    @Override
                    public void run() {
                        if (!installAllBtn.isDisposed() && enabledButtonCount.get() == 0) {
                            installAllBtn.setEnabled(false);
                        }
                    }
                });
            }
        }
    }

    /**
     * DOC sgandon Comment method "individualDownloadFailed".
     * 
     * @param enabledButtonCount
     * @param button
     */
    protected void individualDownloadFailed(final AtomicInteger enabledButtonCount, final Button button) {
        Display.getDefault().asyncExec(new Runnable() {

            @Override
            public void run() {
                if (!button.isDisposed()) {
                    button.setEnabled(true);
                    enabledButtonCount.incrementAndGet();
                    installAllBtn.setEnabled(true);
                }
            }
        });
    }

    /**
     * show the dialog
     * 
     * @param block, whether the this method is blocked until the dialog is closed
     * @param requiredJars, list of required jars
     */
    public void showDialog(boolean block, String[] requiredJars) {
        if (ArrayUtils.contains(Platform.getApplicationArgs(),
                EclipseCommandLine.TALEND_DISABLE_EXTERNAL_MODULE_INSTALL_DIALOG_COMMAND)) {
            CommonExceptionHandler.warn("missing jars: " + ArrayUtils.toString(requiredJars)); //$NON-NLS-1$
            return;
        }

        List<ModuleNeeded> requiredModules = new ArrayList<ModuleNeeded>();
        for (String jarName : requiredJars) {
            // create module without mvnuri and handle it in RemoteModulesHelper.getNotInstalledModulesRunnable
            requiredModules.add(new ModuleNeeded(null, jarName, null, true));
        }
        showDialog(block, requiredModules);
    }

    public void showDialog(boolean block, Collection<ModuleNeeded> requiredModules) {
        if (ArrayUtils.contains(Platform.getApplicationArgs(),
                EclipseCommandLine.TALEND_DISABLE_EXTERNAL_MODULE_INSTALL_DIALOG_COMMAND)) {
            CommonExceptionHandler.warn("missing jars: " + ArrayUtils.toString(requiredModules)); //$NON-NLS-1$
            return;
        }
        // fork = !block;
        // remove duplicated
        List<ModuleNeeded> required = new ArrayList<ModuleNeeded>(requiredModules);
        IRunnableWithProgress notInstalledModulesRunnable = RemoteModulesHelper.getInstance()
                .getNotInstalledModulesRunnable(required, inputList, true);
        setBlockOnOpen(block);
        setInitialRunnable(notInstalledModulesRunnable);
        open();

    }

}