org.springframework.ide.eclipse.wizard.gettingstarted.boot.NewSpringBootWizardModel.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.ide.eclipse.wizard.gettingstarted.boot.NewSpringBootWizardModel.java

Source

/*******************************************************************************
 * Copyright (c) 2013 GoPivotal, Inc.
 * 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:
 *     GoPivotal, Inc. - initial API and implementation
 *******************************************************************************/
package org.springframework.ide.eclipse.wizard.gettingstarted.boot;

import java.beans.Expression;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.springframework.ide.eclipse.core.StringUtils;
import org.springframework.ide.eclipse.wizard.WizardPlugin;
import org.springframework.ide.eclipse.wizard.gettingstarted.boot.json.InitializrServiceSpec;
import org.springframework.ide.eclipse.wizard.gettingstarted.boot.json.InitializrServiceSpec.DependencyGroup;
import org.springframework.ide.eclipse.wizard.gettingstarted.boot.json.InitializrServiceSpec.Dependency;
import org.springframework.ide.eclipse.wizard.gettingstarted.boot.json.InitializrServiceSpec.Option;
import org.springframework.ide.eclipse.wizard.gettingstarted.boot.json.InitializrServiceSpec.Type;
import org.springframework.ide.eclipse.wizard.gettingstarted.content.BuildType;
import org.springframework.ide.eclipse.wizard.gettingstarted.content.CodeSet;
import org.springframework.ide.eclipse.wizard.gettingstarted.importing.ImportStrategy;
import org.springframework.ide.eclipse.wizard.gettingstarted.importing.ImportUtils;
import org.springsource.ide.eclipse.commons.core.preferences.StsProperties;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.DownloadManager;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.DownloadableItem;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.URLConnectionFactory;
import org.springsource.ide.eclipse.commons.livexp.core.FieldModel;
import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression;
import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable;
import org.springsource.ide.eclipse.commons.livexp.core.StringFieldModel;
import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult;
import org.springsource.ide.eclipse.commons.livexp.core.Validator;
import org.springsource.ide.eclipse.commons.livexp.core.ValueListener;
import org.springsource.ide.eclipse.commons.livexp.core.validators.NewProjectLocationValidator;
import org.springsource.ide.eclipse.commons.livexp.core.validators.NewProjectNameValidator;
import org.springsource.ide.eclipse.commons.livexp.core.validators.UrlValidator;
import org.springsource.ide.eclipse.commons.livexp.ui.ProjectLocationSection;

/**
 * A ZipUrlImportWizard is a simple wizard in which one can paste a url
 * pointing to a zip file. The zip file is supposed to contain a maven (or gradle)
 * project in the root of the zip.
 */
public class NewSpringBootWizardModel {

    private static final String JSON_CONTENT_TYPE_HEADER = "application/vnd.initializr.v2.1+json";
    private static final Map<String, BuildType> KNOWN_TYPES = new HashMap<String, BuildType>();
    static {
        KNOWN_TYPES.put("gradle-project", BuildType.GRADLE); // New version of initialzr app
        KNOWN_TYPES.put("maven-project", BuildType.MAVEN); // New versions of initialzr app

        KNOWN_TYPES.put("gradle.zip", BuildType.GRADLE); //Legacy, can remove when new initializr app uses "gradle-project" definitively
        KNOWN_TYPES.put("starter.zip", BuildType.MAVEN); //Legacy, can remove when initializr app uses "maven-project" definitively
    }

    /**
     * Lists known query parameters that map onto a String input field. The default values for these
     * parameters will be pulled from the json spec document.
     */
    private static final Map<String, String> KNOWN_STRING_INPUTS = new LinkedHashMap<String, String>();
    static {
        KNOWN_STRING_INPUTS.put("name", "Name");
        KNOWN_STRING_INPUTS.put("groupId", "Group");
        KNOWN_STRING_INPUTS.put("artifactId", "Artifact");
        KNOWN_STRING_INPUTS.put("version", "Version");
        KNOWN_STRING_INPUTS.put("description", "Description");
        KNOWN_STRING_INPUTS.put("packageName", "Package");
    };

    private static final Map<String, String> KNOWN_SINGLE_SELECTS = new LinkedHashMap<String, String>();
    static {
        KNOWN_SINGLE_SELECTS.put("packaging", "Packaging:");
        KNOWN_SINGLE_SELECTS.put("javaVersion", "Java Version:");
        KNOWN_SINGLE_SELECTS.put("language", "Language:");
        KNOWN_SINGLE_SELECTS.put("bootVersion", "Boot Version:");
    }

    private final URLConnectionFactory urlConnectionFactory;
    private final String JSON_URL;
    private final String CONTENT_TYPE;

    private final Map<String, LiveExpression<Boolean>> dependencyEnablement = new HashMap<String, LiveExpression<Boolean>>();

    public NewSpringBootWizardModel() throws Exception {
        this(new URLConnectionFactory(), StsProperties.getInstance(new NullProgressMonitor()));
    }

    public NewSpringBootWizardModel(URLConnectionFactory urlConnectionFactory, StsProperties stsProps)
            throws Exception {
        this(urlConnectionFactory, stsProps.get("spring.initializr.json.url"), JSON_CONTENT_TYPE_HEADER);
    }

    public NewSpringBootWizardModel(URLConnectionFactory urlConnectionFactory, String jsonUrl, String contentType)
            throws Exception {
        this.urlConnectionFactory = urlConnectionFactory;
        this.JSON_URL = jsonUrl;
        this.CONTENT_TYPE = contentType;

        baseUrl = new LiveVariable<String>("<computed>");
        baseUrlValidator = new UrlValidator("Base Url", baseUrl);

        discoverOptions(stringInputs, dependencies);
        dependencies.sort();

        projectName = stringInputs.getField("name");
        projectName.validator(new NewProjectNameValidator(projectName.getVariable()));
        location = new LiveVariable<String>(
                ProjectLocationSection.getDefaultProjectLocation(projectName.getValue()));
        locationValidator = new NewProjectLocationValidator("Location", location, projectName.getVariable());
        Assert.isNotNull(projectName, "The service at " + JSON_URL + " doesn't specify a 'name' text input");

        UrlMaker computedUrl = new UrlMaker(baseUrl);
        for (FieldModel<String> param : stringInputs) {
            computedUrl.addField(param);
        }
        computedUrl.addField(dependencies);
        for (RadioGroup group : radioGroups.getGroups()) {
            computedUrl.addField(group);
        }
        computedUrl.addListener(new ValueListener<String>() {
            public void gotValue(LiveExpression<String> exp, String value) {
                downloadUrl.setValue(value);
            }
        });

        addBuildTypeValidator();
    }

    /**
     * If this wizard has a 'type' radioGroup to select the build type then add a validator to check if the
     * build type is supported.
     */
    private void addBuildTypeValidator() {
        RadioGroup buildTypeGroup = getRadioGroups().getGroup("type");
        if (buildTypeGroup != null) {
            buildTypeGroup.validator(new Validator() {
                @Override
                protected ValidationResult compute() {
                    BuildType bt = getBuildType();
                    if (!bt.getImportStrategy().isSupported()) {
                        //This means some required STS component like m2e or gradle tooling is not installed
                        return ValidationResult.error(bt.getNotInstalledMessage());
                    }
                    return ValidationResult.OK;
                }
            }.dependsOn(buildTypeGroup.getVariable()));
        }
    }

    @SuppressWarnings("unchecked")
    public final FieldArrayModel<String> stringInputs = new FieldArrayModel<String>(
    //The fields need to be discovered by parsing web form.
    );

    public final HierarchicalMultiSelectionFieldModel<Dependency> dependencies = new HierarchicalMultiSelectionFieldModel<Dependency>(
            Dependency.class, "dependencies").label("Dependencies:");

    private final FieldModel<String> projectName; //an alias for stringFields.getField("name");
    private final LiveVariable<String> location;
    private final NewProjectLocationValidator locationValidator;

    private boolean allowUIThread = false;

    public final LiveVariable<String> baseUrl;
    public final LiveExpression<ValidationResult> baseUrlValidator;

    public final LiveVariable<String> downloadUrl = new LiveVariable<String>();
    private IWorkingSet[] workingSets = new IWorkingSet[0];
    private RadioGroups radioGroups = new RadioGroups();
    private RadioGroup bootVersion;

    public void performFinish(IProgressMonitor mon) throws InvocationTargetException, InterruptedException {
        mon.beginTask("Importing " + baseUrl.getValue(), 4);
        DownloadManager downloader = null;
        try {
            downloader = new DownloadManager().allowUIThread(allowUIThread);

            DownloadableItem zip = new DownloadableItem(newURL(downloadUrl.getValue()), downloader);
            String projectNameValue = projectName.getValue();
            CodeSet cs = CodeSet.fromZip(projectNameValue, zip, new Path("/"));

            IRunnableWithProgress oper = getImportStrategy()
                    .createOperation(ImportUtils.importConfig(new Path(location.getValue()), projectNameValue, cs));
            oper.run(new SubProgressMonitor(mon, 3));

            IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectNameValue);
            addToWorkingSets(project, new SubProgressMonitor(mon, 1));

        } catch (IOException e) {
            throw new InvocationTargetException(e);
        } finally {
            if (downloader != null) {
                downloader.dispose();
            }
            mon.done();
        }
    }

    /**
     * Get currently selected import strategy.
     * Never returns null (some default is returned in any case).
     */
    public ImportStrategy getImportStrategy() {
        return getBuildType().getImportStrategy();
    }

    /**
     * Gets the currently selected BuildType.
     * Never returns null (some default is returned in any case).
     */
    public BuildType getBuildType() {
        RadioGroup buildTypeRadios = getRadioGroups().getGroup("type");
        if (buildTypeRadios != null) {
            RadioInfo selected = buildTypeRadios.getSelection().selection.getValue();
            if (selected != null) {
                BuildType bt = KNOWN_TYPES.get(selected.getValue());
                if (bt != null) {
                    return bt;
                } else {
                    //Uknown build type, import it as a general project which is better than nothing
                    return BuildType.GENERAL;
                }
            }
        }
        //Old initialzr app doesn't have button to specify build type... it is always maven
        return BuildType.MAVEN;
    }

    private void addToWorkingSets(IProject project, IProgressMonitor monitor) {
        monitor.beginTask("Add '" + project.getName() + "' to working sets", 1);
        try {
            if (workingSets == null || workingSets.length == 0) {
                return;
            }
            IWorkingSetManager wsm = PlatformUI.getWorkbench().getWorkingSetManager();
            wsm.addToWorkingSets(project, workingSets);
        } finally {
            monitor.done();
        }
    }

    /**
     * Dynamically discover input fields and 'style' options by parsing initializr form.
     */
    private void discoverOptions(FieldArrayModel<String> fields,
            HierarchicalMultiSelectionFieldModel<Dependency> dependencies) throws Exception {
        InitializrServiceSpec serviceSpec = parseJsonFrom(new URL(JSON_URL));

        Map<String, String> textInputs = serviceSpec.getTextInputs();
        for (Entry<String, String> e : KNOWN_STRING_INPUTS.entrySet()) {
            String name = e.getKey();
            String defaultValue = textInputs.get(name);
            if (defaultValue != null) {
                fields.add(new StringFieldModel(name, defaultValue).label(e.getValue()));
            }
        }

        { //field: type
            String groupName = "type";
            RadioGroup group = radioGroups.ensureGroup(groupName);
            group.label("Type:");
            for (Type type : serviceSpec.getTypeOptions(groupName)) {
                if (KNOWN_TYPES.containsKey(type.getId())) {
                    TypeRadioInfo radio = new TypeRadioInfo(groupName, type.getId(), type.isDefault(),
                            type.getAction());
                    radio.setLabel(type.getName());
                    group.add(radio);
                }
            }
            //When a type is selected the 'baseUrl' should be update according to its action.
            group.getSelection().selection.addListener(new ValueListener<RadioInfo>() {
                public void gotValue(LiveExpression<RadioInfo> exp, RadioInfo value) {
                    try {
                        if (value != null) {
                            URI base = new URI(JSON_URL);
                            URI resolved = base.resolve(((TypeRadioInfo) value).getAction());
                            baseUrl.setValue(resolved.toString());
                        }
                    } catch (Exception e) {
                        WizardPlugin.log(e);
                    }
                }
            });
        }

        for (Entry<String, String> e : KNOWN_SINGLE_SELECTS.entrySet()) {
            String groupName = e.getKey();
            RadioGroup group = radioGroups.ensureGroup(groupName);
            group.label(e.getValue());
            addOptions(group, serviceSpec.getSingleSelectOptions(groupName));
            if (groupName.equals("bootVersion")) {
                this.bootVersion = group;
            }
        }

        //styles
        for (DependencyGroup dgroup : serviceSpec.getDependencies()) {
            String catName = dgroup.getName();
            for (Dependency dep : dgroup.getContent()) {
                dependencies.choice(catName, dep.getName(), dep, dep.getDescription(),
                        createEnablementExp(bootVersion, dep.getVersionRange()));
            }
        }
    }

    private LiveExpression<Boolean> createEnablementExp(final RadioGroup bootVersion, String versionRange) {
        try {
            if (StringUtils.hasText(versionRange)) {
                final VersionRange range = new VersionRange(versionRange);
                return new LiveExpression<Boolean>() {
                    {
                        dependsOn(bootVersion.getSelection().selection);
                    }

                    @Override
                    protected Boolean compute() {
                        try {
                            String versionStr = bootVersion.getSelection().selection.getValue().getValue();
                            if (versionStr != null) {
                                Version version = new Version(versionStr);
                                return range.includes(version);
                            }
                        } catch (Exception e) {
                            WizardPlugin.log(e);
                        }
                        return true;
                    }
                };
            }
        } catch (Exception e) {
            WizardPlugin.log(e);
        }
        return LiveExpression.TRUE;
    }

    private void addOptions(RadioGroup group, Option[] options) {
        for (Option option : options) {
            RadioInfo radio = new RadioInfo(group.getName(), option.getId(), option.isDefault());
            radio.setLabel(option.getName());
            group.add(radio);
        }
    }

    private InitializrServiceSpec parseJsonFrom(URL url) throws Exception {
        URLConnection conn = null;
        InputStream input = null;
        try {
            conn = urlConnectionFactory.createConnection(url);
            conn.addRequestProperty("User-Agent", "STS " + WizardPlugin.getDefault().getBundle().getVersion());
            if (CONTENT_TYPE != null) {
                conn.addRequestProperty("Accept", CONTENT_TYPE);
            }
            conn.connect();
            input = conn.getInputStream();
            return InitializrServiceSpec.parseFrom(input);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                }
            }
        }
    }

    private URL newURL(String value) {
        try {
            return new URL(value);
        } catch (MalformedURLException e) {
            //This should be impossible because the URL syntax is validated beforehand.
            WizardPlugin.log(e);
            return null;
        }
    }

    /**
     * This is mostly for testing purposes where it is just easier to run stuff in the UIThread (test do so
     * by default). But in production we shouldn't allow downloading stuff in the UIThread.
     */
    public void allowUIThread(boolean allow) {
        this.allowUIThread = allow;
    }

    public LiveExpression<ValidationResult> getLocationValidator() {
        return locationValidator;
    }

    public LiveVariable<String> getLocation() {
        return location;
    }

    public FieldModel<String> getProjectName() {
        return projectName;
    }

    public void setWorkingSets(IWorkingSet[] workingSets) {
        this.workingSets = workingSets;
    }

    public RadioGroups getRadioGroups() {
        return this.radioGroups;
    }

    public RadioGroup getBootVersion() {
        return bootVersion;
    }

}