/*******************************************************************************
* 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.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.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.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.Depependency;
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 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 HashMap<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("packgageName", "Package");
};
private final URLConnectionFactory urlConnectionFactory;
private final String JSON_URL;
private final String CONTENT_TYPE;
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"), stsProps.get("spring.initializr.json.contentType"));
}
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, style);
style.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(style);
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 MultiSelectionFieldModel<String> style = new MultiSelectionFieldModel<String>(String.class, "style")
.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();
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, MultiSelectionFieldModel<String> style) throws Exception {
InitializrServiceSpec serviceSpec = parseJsonFrom(new URL(JSON_URL));
for (Entry<String, String> e : KNOWN_STRING_INPUTS.entrySet()) {
String name = e.getKey();
String defaultValue = serviceSpec.getDefaults().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.getTypes()) {
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);
}
}
});
}
{ //field: packaging
String groupName = "packaging";
RadioGroup group = radioGroups.ensureGroup(groupName);
group.label("Packaging:");
addOptions(group, serviceSpec.getPackagings());
}
{ //field: javaVersion
String groupName = "javaVersion";
RadioGroup group = radioGroups.ensureGroup(groupName);
group.label("Java Version:");
addOptions(group, serviceSpec.getJavaVersions());
}
{ //field: language
String groupName = "language";
RadioGroup group = radioGroups.ensureGroup(groupName);
group.label("Language:");
addOptions(group, serviceSpec.getLanguages());
}
{ //field: bootVersion
String groupName = "bootVersion";
RadioGroup group = radioGroups.ensureGroup(groupName);
group.label("Boot Version:");
addOptions(group, serviceSpec.getBootVersions());
}
//styles
for (DependencyGroup dgroup : serviceSpec.getDependencies()) {
for (Depependency dep : dgroup.getContent()) {
style.choice(dep.getName(), dep.getId(), dep.getDescription());
}
}
}
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);
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;
}
}