org.eclipse.n4js.ui.wizard.project.N4JSNewProjectWizardCreationPage.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.n4js.ui.wizard.project.N4JSNewProjectWizardCreationPage.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 org.eclipse.n4js.ui.wizard.project;

import static com.google.common.base.CharMatcher.breakingWhitespace;
import static com.google.common.base.CharMatcher.inRange;
import static com.google.common.base.CharMatcher.is;
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.collect.Lists.newArrayList;
import static org.eclipse.jface.layout.GridDataFactory.fillDefaults;
import static org.eclipse.n4js.projectDescription.ProjectType.API;
import static org.eclipse.n4js.projectDescription.ProjectType.LIBRARY;
import static org.eclipse.n4js.projectDescription.ProjectType.TEST;
import static org.eclipse.n4js.ui.wizard.project.N4JSProjectInfo.IMPLEMENTATION_ID_PROP_NAME;
import static org.eclipse.n4js.ui.wizard.project.N4JSProjectInfo.IMPLEMENTED_PROJECTS_PROP_NAME;
import static org.eclipse.n4js.ui.wizard.project.N4JSProjectInfo.PROJECT_TYPE_PROP_NAME;
import static org.eclipse.swt.SWT.BORDER;
import static org.eclipse.swt.SWT.CHECK;
import static org.eclipse.swt.SWT.FILL;
import static org.eclipse.swt.SWT.MULTI;
import static org.eclipse.swt.SWT.Modify;
import static org.eclipse.swt.SWT.READ_ONLY;
import static org.eclipse.xtext.util.Strings.toFirstUpper;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.beans.typed.PojoProperties;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.databinding.viewers.typed.ViewerProperties;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.ListViewer;
import org.eclipse.n4js.json.JSON.JSONPackage;
import org.eclipse.n4js.projectDescription.ProjectType;
import org.eclipse.n4js.projectModel.IN4JSProject;
import org.eclipse.n4js.resource.packagejson.PackageJsonResourceDescriptionExtension;
import org.eclipse.n4js.ui.internal.N4JSActivator;
import org.eclipse.n4js.utils.ProjectDescriptionUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.layout.GridData;
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.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.dialogs.ExtensibleWizardNewProjectCreationPage;
import org.eclipse.xtext.resource.IResourceDescriptions;

import com.google.inject.Injector;

/**
 * Wizard page for configuring a new N4JS project.
 */
public class N4JSNewProjectWizardCreationPage extends ExtensibleWizardNewProjectCreationPage {

    private final N4JSProjectInfo projectInfo;

    // RegEx pattern for valid vendor IDs. See terminal ID rule in the N4MF grammar.
    private static final Pattern VENDOR_ID_PATTERN = Pattern.compile("\\^?[A-Za-z\\_][A-Za-z_\\-\\.0-9]*");

    /**
     * Creates a new wizard page to set up and create a new N4JS project with the given project info model.
     *
     * @param projectInfo
     *            the project info model that will be used to initialize the new N4JS project.
     */
    public N4JSNewProjectWizardCreationPage(final N4JSProjectInfo projectInfo) {
        super(N4JSNewProjectWizardCreationPage.class.getName());
        this.projectInfo = projectInfo;
        setTitle("N4JS Project");
        setDescription("Create a new N4JS project.");
    }

    @Override
    public boolean canFlipToNextPage() {
        // Only allow page flipping for test projects
        return isPageComplete() && TEST.equals(projectInfo.getProjectType());
    }

    @Override
    public void createControl(final Composite parent) {
        super.createControl(parent); // We need to create the UI controls from the parent class.

        final Composite control = (Composite) getControl();
        control.setLayout(GridLayoutFactory.fillDefaults().create());
        control.setLayoutData(fillDefaults().align(FILL, FILL).grab(true, true).create());

        final DataBindingContext dbc = new DataBindingContext();
        control.addDisposeListener(e -> dbc.dispose());

        createVendorIdControls(dbc, control);

        ComboViewer projectTypeCombo = new ComboViewer(control, READ_ONLY);
        projectTypeCombo.setLabelProvider(new ProjectTypeLabelProvider());
        projectTypeCombo.setContentProvider(ArrayContentProvider.getInstance());
        projectTypeCombo.getControl().setLayoutData(fillDefaults().grab(true, false).create());
        projectTypeCombo.setInput(ProjectType.values());

        Composite projectTypePropertyControls = new Composite(control, NONE);
        StackLayout changingStackLayout = new StackLayout();
        projectTypePropertyControls.setLayout(changingStackLayout);
        projectTypePropertyControls.setLayoutData(fillDefaults().align(FILL, FILL).grab(true, true).create());

        Composite defaultOptions = initDefaultOptionsUI(dbc, projectTypePropertyControls);
        Composite libraryProjectOptionsGroup = initLibraryOptionsUI(dbc, projectTypePropertyControls);
        Composite testProjectOptionsGroup = initTestProjectUI(dbc, projectTypePropertyControls);

        initProjectTypeBinding(dbc, projectTypeCombo);

        // Configure stack layout to show advanced options
        projectTypeCombo.addPostSelectionChangedListener(e -> {
            switch (projectInfo.getProjectType()) {
            case LIBRARY:
                changingStackLayout.topControl = libraryProjectOptionsGroup;
                break;
            case TEST:
                changingStackLayout.topControl = testProjectOptionsGroup;
                break;
            default:
                changingStackLayout.topControl = defaultOptions;
            }
            projectTypePropertyControls.layout(true);
            setPageComplete(validatePage());
        });

        // IDs from: org.eclipse.jdt.internal.ui.workingsets.IWorkingSetIDs.class
        createWorkingSetGroup((Composite) getControl(), null,
                new String[] { "org.eclipse.ui.resourceWorkingSetPage", "org.eclipse.jdt.ui.JavaWorkingSetPage",
                        "org.eclipse.jdt.internal.ui.OthersWorkingSet" }); // $NON-NLS-1$
        Dialog.applyDialogFont(getControl());

        dbc.updateTargets();

        setControl(control);
    }

    @Override
    public String getProjectName() {
        return ProjectDescriptionUtils.getPlainProjectName(super.getProjectName());
    }

    @Override
    public IProject getProjectHandle() {
        // make sure the validated project handle considers the eclipse representation
        // of scoped projects
        final String eclipseProjectName = ProjectDescriptionUtils
                .convertN4JSProjectNameToEclipseProjectName(getProjectName());
        return ResourcesPlugin.getWorkspace().getRoot().getProject(eclipseProjectName);
    }

    /**
     * Returns the full project name including the scope (e.g. {@code @scope/*}) prefix.
     */
    protected String getProjectNameWithScope() {
        return super.getProjectName();
    }

    @Override
    @SuppressWarnings("restriction")
    protected void setLocationForSelection() {
        // use scoped project name for project location
        locationArea.updateProjectName(getProjectNameWithScope());
    }

    private void createVendorIdControls(DataBindingContext dbc, Composite parent) {
        final Composite composite = new Composite(parent, SWT.NULL);
        composite.setLayout(GridLayoutFactory.swtDefaults().numColumns(2).create());
        composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        final Label vendorIdLabel = new Label(composite, SWT.NONE);
        vendorIdLabel.setText("Vendor id:");

        Text vendorIdText = new Text(composite, SWT.BORDER);
        vendorIdText.setLayoutData(fillDefaults().align(FILL, FILL).grab(true, true).create());

        projectInfo.addPropertyChangeListener(event -> {
            if (event.getPropertyName().equals(N4JSProjectInfo.VENDOR_ID_PROP_NAME)) {
                setPageComplete(validatePage());
            }
        });

        dbc.bindValue(WidgetProperties.text(Modify).observe(vendorIdText), BeanProperties
                .value(N4JSProjectInfo.class, N4JSProjectInfo.VENDOR_ID_PROP_NAME).observe(projectInfo));

    }

    private Composite initDefaultOptionsUI(DataBindingContext dbc, Composite parent) {
        // A group for default options
        final Group defaultOptions = new Group(parent, NONE);
        defaultOptions.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).create());

        final Button createGreeterFileButton = new Button(defaultOptions, CHECK);
        createGreeterFileButton.setText("Create a greeter file");

        initDefaultCreateGreeterBindings(dbc, createGreeterFileButton);

        return defaultOptions;
    }

    private Composite initLibraryOptionsUI(DataBindingContext dbc, Composite parent) {
        // Additional library project options
        final Group libraryProjectOptionsGroup = new Group(parent, NONE);
        libraryProjectOptionsGroup.setLayout(
                GridLayoutFactory.fillDefaults().margins(12, 5).numColumns(2).equalWidth(false).create());

        emptyPlaceholder(libraryProjectOptionsGroup);

        final Button createGreeterFileButton = new Button(libraryProjectOptionsGroup, CHECK);
        createGreeterFileButton.setText("Create a greeter file");
        createGreeterFileButton.setLayoutData(GridDataFactory.fillDefaults().create());

        new Label(libraryProjectOptionsGroup, SWT.NONE).setText("Implementation ID:");
        final Text implementationIdText = new Text(libraryProjectOptionsGroup, BORDER);
        implementationIdText.setLayoutData(fillDefaults().align(FILL, SWT.CENTER).grab(true, false).create());

        final Label implementedProjectsLabel = new Label(libraryProjectOptionsGroup, SWT.NONE);
        implementedProjectsLabel.setText("Implemented projects:");
        implementedProjectsLabel
                .setLayoutData(GridDataFactory.fillDefaults().grab(false, true).align(SWT.LEFT, SWT.TOP).create());

        final ListViewer apiViewer = new ListViewer(libraryProjectOptionsGroup, BORDER | MULTI);
        apiViewer.getControl().setLayoutData(fillDefaults().align(FILL, FILL).grab(true, true).span(1, 1).create());
        apiViewer.setContentProvider(ArrayContentProvider.getInstance());
        apiViewer.setInput(getAvailableApiProjectNames());

        initApiViewerBinding(dbc, apiViewer);
        initImplementationIdBinding(dbc, implementationIdText);
        initDefaultCreateGreeterBindings(dbc, createGreeterFileButton);

        // Invalidate on change
        apiViewer.addSelectionChangedListener(e -> {
            setPageComplete(validatePage());
        });
        // Invalidate on change
        implementationIdText.addModifyListener(e -> {
            setPageComplete(validatePage());
        });

        return libraryProjectOptionsGroup;
    }

    /** Create an empty placeholder control in parent */
    private static Control emptyPlaceholder(Composite parent) {
        return new Label(parent, NONE);
    }

    private Composite initTestProjectUI(DataBindingContext dbc, Composite parent) {
        // Additional test project options
        final Group testProjectOptionsGroup = new Group(parent, NONE);
        testProjectOptionsGroup.setLayout(GridLayoutFactory.fillDefaults().numColumns(1).create());

        final Button createTestGreeterFileButton = new Button(testProjectOptionsGroup, CHECK);
        createTestGreeterFileButton.setText("Create a test project greeter file");

        final Button addNormalSourceFolderButton = new Button(testProjectOptionsGroup, CHECK);
        addNormalSourceFolderButton.setText("Also create a non-test source folder");

        Label nextPageHint = new Label(testProjectOptionsGroup, NONE);
        nextPageHint.setText("The projects which should be tested can be selected on the next page");
        nextPageHint.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_TITLE_INACTIVE_FOREGROUND));

        initTestProjectBinding(dbc, addNormalSourceFolderButton, createTestGreeterFileButton);

        return testProjectOptionsGroup;
    }

    @Override
    protected boolean validatePage() {
        final boolean valid = validateProjectNameNonEmpty() && super.validatePage()
                && validateIsExistingProjectPath() && validateProjectName() && validateVendorId()
                && validateImplementationId();

        if (valid) {
            // clear error messages
            setErrorMessage(null);
            // update model only if validation passed successfully
            updateModel();
        }

        return valid;
    }

    /** Returns {@code true} iff the specified project name is not considered empty. */
    private boolean validateProjectNameNonEmpty() {
        final String fullProjectName = getProjectNameWithScope();
        final String scopeName = ProjectDescriptionUtils.getScopeName(fullProjectName);
        final String plainProjectName = ProjectDescriptionUtils.getPlainProjectName(fullProjectName);

        if (fullProjectName.isEmpty()) {
            setErrorMessage(null);
            setMessage("Project name must be specified");
            return false;
        }

        if (scopeName != null && (scopeName.isEmpty() || scopeName.equals("@"))) {
            setErrorMessage("The scope segment of the project name must not be empty.");
            return false;
        }

        if (plainProjectName != null && plainProjectName.isEmpty()) {
            setErrorMessage("The name segment of the project name must not be empty.");
            return false;
        }

        return true;
    }

    /** Returns {@code true} iff the specified project name is valid. */
    private boolean validateProjectName() {
        final String projectName = getProjectNameWithScope();
        final String plainProjectName = getProjectName();
        final String scopeName = ProjectDescriptionUtils.getScopeName(projectName);

        if (scopeName != null && !ProjectDescriptionUtils.isValidScopeName(scopeName)) {
            setErrorMessage("Invalid project name: \"" + scopeName + "\" is not a valid scope segment.");
            return false;
        }

        if (!ProjectDescriptionUtils.isValidPlainProjectName(plainProjectName)) {
            setErrorMessage("Invalid project name: \"" + plainProjectName + "\" is not a valid name segment.");
            return false;
        }

        if (breakingWhitespace().matchesAnyOf(projectName)) {
            setErrorMessage("Project name should not contain any whitespace characters.");
            return false;
        }

        if (isNullOrEmpty(projectName)) {
            setErrorMessage("Project name should be specified.");
            return false;
        }

        return true;
    }

    /** Returns {@code true} iff the vendor ID is valid. */
    private boolean validateVendorId() {
        final String vendorId = projectInfo.getVendorId();
        if (isNullOrEmpty(vendorId)) {
            setErrorMessage("Vendor id must not be empty.");
            return false;
        }

        if (!VENDOR_ID_PATTERN.matcher(vendorId).matches()) {
            setErrorMessage("Invalid vendor id.");
            return false;
        }
        return true;
    }

    /** Returns {@code true} iff the implementation ID is valid. */
    private boolean validateImplementationId() {
        if (LIBRARY.equals(projectInfo.getProjectType())) {
            final String implementationId = projectInfo.getImplementationId();

            // Implementation ID is optional
            if (!isNullOrEmpty(implementationId)) {
                final char leadingChar = implementationId.charAt(0);

                if (is('_').or(inRange('a', 'z').or(inRange('A', 'Z'))).matches(leadingChar)) {
                    setErrorMessage("Implementation ID should start either an upper or a lower case character "
                            + "from the Latin alphabet or with the underscore character.");
                    return false;
                }
                final List<String> implementedApis = projectInfo.getImplementedProjects();
                if (null == implementedApis || implementedApis.isEmpty()) {
                    setErrorMessage(
                            "One or more API project should be selected for implementation when the implementation ID is specified.");
                    return false;
                }

                if (breakingWhitespace().matchesAnyOf(implementationId)) {
                    setErrorMessage("Implementation ID should not contain any whitespace characters.");
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Accessible reader from outside. Call before using the project info.
     */
    public void updateSelectedWorkingSets() {
        projectInfo.setSelectedWorkingSets(getSelectedWorkingSets());
    }

    private void updateModel() {
        // use eclipse project name in model
        final String eclipseProjectName = ProjectDescriptionUtils
                .convertN4JSProjectNameToEclipseProjectName(getProjectNameWithScope());
        projectInfo.setProjectName(eclipseProjectName);

        final boolean isScoped = getProjectNameWithScope() != getProjectName();
        if (useDefaults() && isScoped) {
            projectInfo.setProjectLocation(getLocationPath().append(getProjectNameWithScope()));
        } else if (useDefaults()) {
            projectInfo.setProjectLocation(null);
        } else {
            projectInfo.setProjectLocation(getLocationPath());
        }
    }

    private void initDefaultCreateGreeterBindings(DataBindingContext dbc, Button createGreeterFileButton) {
        // Bind the "create greeter file"-checkbox
        dbc.bindValue(WidgetProperties.buttonSelection().observe(createGreeterFileButton), BeanProperties
                .value(N4JSProjectInfo.class, N4JSProjectInfo.CREATE_GREETER_FILE_PROP_NAME).observe(projectInfo));
    }

    private void initTestProjectBinding(DataBindingContext dbc, Button addNormalSourceFolderButton,
            Button createTestGreeterFileButton) {
        // Bind the "normal source folder"-checkbox
        dbc.bindValue(WidgetProperties.buttonSelection().observe(addNormalSourceFolderButton),
                PojoProperties
                        .value(N4JSProjectInfo.class, N4JSProjectInfo.ADDITIONAL_NORMAL_SOURCE_FOLDER_PROP_NAME)
                        .observe(projectInfo));

        // Bind the "Create greeter file"-checkbox
        dbc.bindValue(WidgetProperties.buttonSelection().observe(createTestGreeterFileButton), BeanProperties
                .value(N4JSProjectInfo.class, N4JSProjectInfo.CREATE_GREETER_FILE_PROP_NAME).observe(projectInfo));
    }

    private void initProjectTypeBinding(final DataBindingContext dbc, final ComboViewer projectType) {
        dbc.bindValue(ViewerProperties.singleSelection().observe(projectType),
                PojoProperties.value(N4JSProjectInfo.class, PROJECT_TYPE_PROP_NAME).observe(projectInfo));
    }

    private void initImplementationIdBinding(final DataBindingContext dbc, final Text text) {
        dbc.bindValue(WidgetProperties.text(Modify).observe(text),
                PojoProperties.value(N4JSProjectInfo.class, IMPLEMENTATION_ID_PROP_NAME).observe(projectInfo));
    }

    private void initApiViewerBinding(DataBindingContext dbc, ListViewer apiViewer) {
        dbc.bindList(ViewerProperties.multipleSelection().observe(apiViewer),
                PojoProperties.list(N4JSProjectInfo.class, IMPLEMENTED_PROJECTS_PROP_NAME).observe(projectInfo));
    }

    /**
     * Checks whether the specified project path points to an existing project and sets an according error message.
     *
     * Returns <code>true</code> otherwise.
     *
     * This method assumes that {@link #getProjectName()} returns a valid project name.
     */
    private boolean validateIsExistingProjectPath() {
        IPath projectLocation = getLocationPath();
        final String projectName = getProjectName();

        // if workspace is project location (default location)
        if (projectLocation.equals(Platform.getLocation())) {
            // add project name since #getLocationPath does not return the full project location
            projectLocation = projectLocation.append(projectName);
        }

        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        boolean workspaceProjectExists = root.getProject(projectName).exists();

        // check for an existing project description file
        IPath projectDescriptionPath = projectLocation.append(IN4JSProject.PACKAGE_JSON);
        File existingProjectDescriptionFile = new File(projectDescriptionPath.toString());

        // check for an existing file with the path of the project folder
        File existingFileAtProjectDirectory = new File(projectLocation.toString());
        boolean projectDirectoryIsExistingFile = existingFileAtProjectDirectory.isFile();
        boolean isExistingNonWorkspaceProject = existingProjectDescriptionFile.exists() && !workspaceProjectExists;

        if (projectDirectoryIsExistingFile) {
            // set error message if there is already at the specified project location
            setErrorMessage("There already exists a file at the location '" + projectLocation.toString() + "'.");
            return false;
        } else if (isExistingNonWorkspaceProject) {
            // set error message if the specified directory already represents an N4JS project
            setErrorMessage(
                    "There already exists an N4JS project at the specified location. Please use 'File > Import...' to add it to the workspace.");
            return false;
        } else {
            // otherwise the project location does not exist yet
            return true;
        }
    }

    private Collection<String> getAvailableApiProjectNames() {
        final Collection<String> distinctIds = from(
                getResourceDescriptions().getExportedObjectsByType(JSONPackage.Literals.JSON_DOCUMENT))
                        .filter(desc -> API.equals(PackageJsonResourceDescriptionExtension.getProjectType(desc)))
                        .transform(desc -> PackageJsonResourceDescriptionExtension.getProjectName(desc))
                        .filter(id -> null != id).toSet();
        final List<String> ids = newArrayList(distinctIds);
        Collections.sort(ids);
        return ids;
    }

    private IResourceDescriptions getResourceDescriptions() {
        return getInjector().getInstance(IResourceDescriptions.class);
    }

    private Injector getInjector() {
        return N4JSActivator.getInstance().getInjector(N4JSActivator.ORG_ECLIPSE_N4JS_N4JS);
    }

    /**
     * Label provider for converting the {@link ProjectType} literal into a human readable string.
     */
    private static final class ProjectTypeLabelProvider extends LabelProvider {
        @Override
        public String getText(final Object element) {
            if (API.equals(element)) {
                return API.getLiteral();
            }
            return getDefaultText(element);
        }

        private String getDefaultText(final Object element) {
            return toFirstUpper(nullToEmpty(super.getText(element)).replaceAll("_", " ").toLowerCase());
        }
    }

}