eu.numberfour.n4js.ui.preferences.ExternalLibraryPreferencePage.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.ui.preferences.ExternalLibraryPreferencePage.java

Source

/**
 * Copyright (c) 2016 NumberFour AG.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */
package eu.numberfour.n4js.ui.preferences;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.primitives.Ints.asList;
import static eu.numberfour.n4js.external.libraries.ExternalLibrariesActivator.EXTERNAL_LIBRARIES_SUPPLIER;
import static eu.numberfour.n4js.external.libraries.ExternalLibrariesActivator.EXTERNAL_LIBRARY_NAMES;
import static eu.numberfour.n4js.external.libraries.TargetPlatformModel.TP_FILTER_EXTENSION;
import static eu.numberfour.n4js.n4mf.ProjectType.API;
import static eu.numberfour.n4js.ui.utils.UIUtils.getDisplay;
import static java.util.Collections.singletonList;
import static org.eclipse.jface.dialogs.MessageDialog.openError;
import static org.eclipse.jface.layout.GridDataFactory.fillDefaults;
import static org.eclipse.jface.viewers.StyledString.DECORATIONS_STYLER;
import static org.eclipse.swt.SWT.CENTER;
import static org.eclipse.swt.SWT.END;
import static org.eclipse.swt.SWT.FILL;
import static org.eclipse.swt.SWT.OPEN;
import static org.eclipse.swt.SWT.PUSH;
import static org.eclipse.swt.SWT.SAVE;
import static org.eclipse.swt.SWT.Selection;
import static org.eclipse.swt.SWT.TOP;
import static org.eclipse.xtext.util.Strings.toFirstUpper;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
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.DirectoryDialog;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.xtext.xbase.lib.StringExtensions;

import com.google.inject.Inject;
import com.google.inject.Provider;

import eu.numberfour.n4js.binaries.IllegalBinaryStateException;
import eu.numberfour.n4js.external.ExternalLibrariesReloadHelper;
import eu.numberfour.n4js.external.ExternalLibraryWorkspace;
import eu.numberfour.n4js.external.NpmManager;
import eu.numberfour.n4js.external.TargetPlatformInstallLocationProvider;
import eu.numberfour.n4js.external.libraries.TargetPlatformModel;
import eu.numberfour.n4js.n4mf.ProjectType;
import eu.numberfour.n4js.preferences.ExternalLibraryPreferenceStore;
import eu.numberfour.n4js.projectModel.IN4JSProject;
import eu.numberfour.n4js.ui.ImageDescriptorCache.ImageRef;
import eu.numberfour.n4js.ui.binaries.IllegalBinaryStateDialog;
import eu.numberfour.n4js.ui.internal.N4JSActivator;
import eu.numberfour.n4js.ui.utils.UIUtils;
import eu.numberfour.n4js.ui.viewer.TreeViewerBuilder;
import eu.numberfour.n4js.utils.Arrays2;

/**
 * Preference page for managing external libraries.
 */
public class ExternalLibraryPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {

    /**
     * The unique preference page ID.
     */
    public static final String ID = ExternalLibraryPreferencePage.class.getName();

    private static final Map<URI, String> BUILT_IN_LIBS = EXTERNAL_LIBRARIES_SUPPLIER.get();

    @Inject
    private ExternalLibraryPreferenceStore store;

    @Inject
    private Provider<ExternalLibraryTreeContentProvider> contentProvider;

    @Inject
    private NpmManager npmManager;

    @Inject
    private ExternalLibraryWorkspace externalLibraryWorkspace;

    @Inject
    private TargetPlatformInstallLocationProvider installLocationProvider;

    @Inject
    private ExternalLibrariesReloadHelper externalLibrariesReloadHelper;

    private TreeViewer viewer;

    @Override
    public void init(final IWorkbench workbench) {
        // Nothing.
    }

    @Override
    protected Control createContents(final Composite parent) {

        final Composite control = new Composite(parent, NONE);
        control.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).equalWidth(false).create());
        control.setLayoutData(fillDefaults().align(FILL, FILL).create());

        viewer = new TreeViewerBuilder(singletonList(""), contentProvider.get()).setVirtual(true)
                .setHeaderVisible(false).setHasBorder(true).setColumnWeights(asList(1))
                .setLabelProvider(new DelegatingStyledCellLabelProvider(new BuiltInLibrariesLabelProvider()))
                .build(control);

        setViewerInput();

        final Composite subComposite = new Composite(control, NONE);
        subComposite.setLayout(GridLayoutFactory.fillDefaults().create());
        subComposite.setLayoutData(fillDefaults().align(END, TOP).create());

        createButton(subComposite, "Add...", new AddButtonSelectionListener());
        final Button remove = createButton(subComposite, "Remove", new RemoveButtonSelectionListener());
        remove.setEnabled(false);

        createPlaceHolderLabel(subComposite);

        final Button moveUp = createButton(subComposite, "Up", new MoveUpButtonSelectionListener());
        final Button moveDown = createButton(subComposite, "Down", new MoveDownButtonSelectionListener());
        moveUp.setEnabled(false);
        moveDown.setEnabled(false);

        createPlaceHolderLabel(subComposite);

        createButton(subComposite, "Reload", new ReloadButtonListener());

        createPlaceHolderLabel(subComposite);

        createButton(subComposite, "Install npm...", new InstallNpmDependencyButtonListener());

        viewer.addSelectionChangedListener(new ISelectionChangedListener() {

            @Override
            public void selectionChanged(final /* @Nullable */ SelectionChangedEvent event) {
                final Tree tree = viewer.getTree();
                final TreeItem[] selection = tree.getSelection();
                if (!Arrays2.isEmpty(selection) && 1 == selection.length && selection[0].getData() instanceof URI) {
                    final URI uri = (URI) selection[0].getData();
                    if (BUILT_IN_LIBS.containsKey(uri)) {
                        remove.setEnabled(false);
                        moveUp.setEnabled(false);
                        moveDown.setEnabled(false);
                    } else {
                        final int selectionIndex = tree.indexOf(selection[0]);
                        final int itemCount = tree.getItemCount();
                        remove.setEnabled(true);
                        if (selectionIndex > 0) {
                            moveUp.setEnabled(
                                    !BUILT_IN_LIBS.containsKey(tree.getItem(selectionIndex - 1).getData()));
                        } else {
                            moveUp.setEnabled(0 != selectionIndex);
                        }
                        moveDown.setEnabled(selectionIndex != itemCount - 1);
                    }
                } else {
                    remove.setEnabled(false);
                    moveUp.setEnabled(false);
                    moveDown.setEnabled(false);
                }
            }
        });

        return control;
    }

    @Override
    public void createControl(Composite parent) {
        super.createControl(parent);
        final Composite buttonParent = getApplyButton().getParent();
        final Layout layout = buttonParent.getLayout();
        if (layout instanceof GridLayout) {
            ((GridLayout) layout).numColumns = ((GridLayout) layout).numColumns + 1;
        }
        createButton(buttonParent, "Export target platform...", new ExportButtonListener());
    }

    @Override
    protected void performApply() {
        try {
            new ProgressMonitorDialog(getShell()).run(true, false, monitor -> {
                final IStatus status = store.save(monitor);
                if (!status.isOK()) {
                    setMessage(status.getMessage(), ERROR);
                } else {
                    updateInput(viewer, store.getLocations());
                }
            });
        } catch (final InvocationTargetException | InterruptedException exc) {
            throw new RuntimeException("Error while building external libraries.", exc);
        }
    }

    @Override
    protected void performDefaults() {
        store.resetDefaults();
        setViewerInput();
    }

    @Override
    public boolean performCancel() {
        store.invalidate();
        return true;
    }

    @Override
    public boolean performOk() {
        store.invalidate();
        return super.performOk();
    }

    private Button createButton(final Composite parent, final String text, final SelectionListener listener) {
        final Button button = new Button(parent, PUSH);
        button.setLayoutData(fillDefaults().align(FILL, CENTER).create());
        button.setText(text);
        if (null != listener) {
            button.addSelectionListener(listener);
            button.addDisposeListener(e -> {
                button.removeSelectionListener(listener);
            });
        }
        return button;
    }

    /**
     * Asynchronously sets the the viewer input with the locations available from the
     * {@link ExternalLibraryPreferenceStore store}.
     */
    private void setViewerInput() {
        if (null != viewer && null != viewer.getControl() && !viewer.getControl().isDisposed()) {
            UIUtils.getDisplay().asyncExec(() -> updateInput(viewer, store.getLocations()));
        }
    }

    private Control createPlaceHolderLabel(final Composite parent) {
        return new Label(parent, NONE);
    }

    /**
     * Simple label provider for external library locations.
     */
    private static class BuiltInLibrariesLabelProvider extends LabelProvider implements IStyledLabelProvider {

        private static final String BUILT_IN_SUFFIX = " [Built-in]";

        @Override
        public String getText(final Object element) {
            if (element instanceof URI) {
                final String externalLibId = BUILT_IN_LIBS.get(element);
                if (!isNullOrEmpty(externalLibId)) {
                    return EXTERNAL_LIBRARY_NAMES.get(externalLibId) + BUILT_IN_SUFFIX;
                }
                return new File((URI) element).getAbsolutePath();
            } else if (element instanceof IN4JSProject) {
                return ((IN4JSProject) element).getArtifactId();
            }
            return super.getText(element);
        }

        @Override
        public Image getImage(final Object element) {
            if (element instanceof URI) {
                return ImageRef.LIB_PATH.asImage().orNull();
            } else if (element instanceof IN4JSProject) {
                return ImageRef.EXTERNAL_LIB_PROJECT.asImage().orNull();
            }
            return super.getImage(element);
        }

        @Override
        public StyledString getStyledText(final Object element) {
            StyledString string = new StyledString(nullToEmpty(getText(element)));
            if (element instanceof URI) {
                final String text = string.getString();
                if (text.endsWith(BUILT_IN_SUFFIX)) {
                    string.setStyle(text.lastIndexOf(BUILT_IN_SUFFIX), BUILT_IN_SUFFIX.length(),
                            DECORATIONS_STYLER);
                }
            } else if (element instanceof IN4JSProject) {
                final ProjectType type = ((IN4JSProject) element).getProjectType();
                final String typeLabel = getProjectTypeLabel(type);
                string = new StyledString(string.getString() + typeLabel);
                string.setStyle(string.getString().lastIndexOf(typeLabel), typeLabel.length(), DECORATIONS_STYLER);
            }
            return string;
        }

        private String getProjectTypeLabel(final ProjectType projectType) {
            final String label;
            if (API.equals(projectType)) {
                label = API.getName();
            } else {
                label = toFirstUpper(nullToEmpty(projectType.getName()).replaceAll("_", " ").toLowerCase());
            }
            return " [" + label + "]";
        }

    }

    /**
     * Selection listener for exporting the target platform file from the UI.
     */
    private class ExportButtonListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent ignored) {
            final FileDialog dialog = new FileDialog(getShell(), SAVE);
            dialog.setFilterExtensions(new String[] { TP_FILTER_EXTENSION });
            dialog.setFileName(TargetPlatformModel.TP_FILE_NAME);
            dialog.setText("Export N4 Target Platform");
            dialog.setOverwrite(true);
            final String value = dialog.open();
            if (!isNullOrEmpty(value)) {
                final URI location = installLocationProvider.getTargetPlatformNodeModulesLocation();
                final Iterable<IProject> projects = externalLibraryWorkspace.getProjects(location);
                final Iterable<String> projectIds = from(projects).transform(p -> p.getName());
                final TargetPlatformModel model = TargetPlatformModel.createFromNpmProjectIds(projectIds);
                final File file = new File(value);
                try {
                    if (!file.exists()) {
                        checkState(file.createNewFile(), "Error while exporting target platform file.");
                    }
                    try (final PrintWriter pw = new PrintWriter(file)) {
                        pw.write(model.toString());
                        pw.flush();
                    }
                } catch (final IOException e) {
                    throw new RuntimeException("Error while exporting target platform file.", e);
                }
            }
        }

    }

    /**
     * Button selection listener for opening up an {@link InputDialog input dialog}, where user can specify npm package
     * name that will be downloaded and installed to the external libraries.
     *
     * Note: this class is not static, so it will hold reference to all services. Make sure to dispose it.
     *
     */
    private class InstallNpmDependencyButtonListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent e) {
            final Collection<String> installedNpmPackageNames = getInstalledNpmPackages();
            final InputDialog dialog = new InputDialog(UIUtils.getShell(), "npm Install",
                    "Specify an npm package name to download and install:", null, new IInputValidator() {

                        @Override
                        public String isValid(final String newText) {

                            if (StringExtensions.isNullOrEmpty(newText)) {
                                return "The npm package name should be specified.";
                            }

                            for (int i = 0; i < newText.length(); i++) {
                                if (Character.isWhitespace(newText.charAt(i))) {
                                    return "The npm package name must not contain any whitespaces.";
                                }
                            }

                            for (int i = 0; i < newText.length(); i++) {
                                if (Character.isUpperCase(newText.charAt(i))) {
                                    return "The npm package name must not contain any upper case letter.";
                                }
                            }

                            if (installedNpmPackageNames.contains(newText)) {
                                return "The npm package '" + newText + "' is already available.";
                            }

                            return null;
                        }
                    });

            dialog.open();
            final String packageName = dialog.getValue();
            if (!StringExtensions.isNullOrEmpty(packageName)) {
                final AtomicReference<IStatus> errorStatusRef = new AtomicReference<>();
                final AtomicReference<IllegalBinaryStateException> illegalBinaryExcRef = new AtomicReference<>();
                try {
                    new ProgressMonitorDialog(UIUtils.getShell()).run(true, false, monitor -> {
                        try {
                            IStatus status = npmManager.installDependency(packageName, monitor);
                            if (status.isOK()) {
                                updateInput(viewer, store.getLocations());
                            } else {
                                // Raise the error dialog just when this is closed.
                                errorStatusRef.set(status);
                            }
                        } catch (final IllegalBinaryStateException ibse) {
                            illegalBinaryExcRef.set(ibse);
                        }
                    });
                } catch (final InvocationTargetException | InterruptedException exc) {
                    throw new RuntimeException("Error while installing npm dependency: '" + packageName + "'.",
                            exc);
                } finally {
                    if (null != illegalBinaryExcRef.get()) {
                        new IllegalBinaryStateDialog(illegalBinaryExcRef.get()).open();
                    } else if (null != errorStatusRef.get()) {
                        N4JSActivator.getInstance().getLog().log(errorStatusRef.get());
                        getDisplay().asyncExec(() -> openError(getShell(), "npm Install Failed",
                                "Error while installing '" + packageName
                                        + "' npm package.\nPlease check your Error Log view for the detailed npm log about the failure."));
                    }
                }
            }
        }

        private Collection<String> getInstalledNpmPackages() {
            final File root = new File(installLocationProvider.getTargetPlatformNodeModulesLocation());
            return from(externalLibraryWorkspace.getProjects(root.toURI())).transform(p -> p.getName()).toSet();
        }

    }

    /**
     * Listener that refreshes any definition files from the local git repository and reloads the external libraries in
     * blocking fashion.
     */
    private class ReloadButtonListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent e) {

            try {
                new MutableProgressMonitorDialog(getShell()).run(true, true, monitor -> {
                    externalLibrariesReloadHelper.reloadLibraries(true, monitor);
                });
            } catch (final InvocationTargetException | InterruptedException exc) {
                throw new RuntimeException("Error while re-building external libraries.", exc);
            }
        }

    }

    /**
     * Selection listener for adding a new external library location.
     */
    private class AddButtonSelectionListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent e) {
            final String directoryPath = new DirectoryDialog(viewer.getControl().getShell(), OPEN).open();
            if (null != directoryPath) {
                final File file = new File(directoryPath);
                if (file.exists() && file.isDirectory()) {
                    store.add(file.toURI());
                    updateInput(viewer, store.getLocations());
                }
            }
        }
    }

    /**
     * Selection listener for adding a new external library location.
     */
    private class RemoveButtonSelectionListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent e) {
            final ISelection selection = viewer.getSelection();
            if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
                final Object element = ((IStructuredSelection) selection).getFirstElement();
                if (element instanceof URI) {
                    store.remove((URI) element);
                    updateInput(viewer, store.getLocations());
                }
            }
        }
    }

    /**
     * Selection listener for moving up an external library location in the list.
     */
    private class MoveUpButtonSelectionListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent e) {
            final ISelection selection = viewer.getSelection();
            if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
                final Object element = ((IStructuredSelection) selection).getFirstElement();
                if (element instanceof URI) {
                    store.moveUp((URI) element);
                    updateInput(viewer, store.getLocations());
                }
            }
        }

    }

    /**
     * Selection listener for moving down an external library location in the list.
     */
    private class MoveDownButtonSelectionListener extends SelectionAdapter {

        @Override
        public void widgetSelected(final SelectionEvent e) {
            final ISelection selection = viewer.getSelection();
            if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
                final Object element = ((IStructuredSelection) selection).getFirstElement();
                if (element instanceof URI) {
                    store.moveDown((URI) element);
                    updateInput(viewer, store.getLocations());
                }
            }
        }

    }

    private static void updateInput(final TreeViewer viewer, final Object input) {
        UIUtils.getDisplay().asyncExec(() -> {
            final Object[] expandedElements = viewer.getExpandedElements();
            final TreePath[] expandedTreePaths = viewer.getExpandedTreePaths();
            viewer.setInput(input);
            viewer.getControl().notifyListeners(Selection, null);
            if (!Arrays2.isEmpty(expandedElements)) {
                viewer.setExpandedElements(expandedElements);
            }
            if (!Arrays2.isEmpty(expandedTreePaths)) {
                viewer.setExpandedTreePaths(expandedTreePaths);
            }
        });
    }

}