/*******************************************************************************
* 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.guides;
import static org.springsource.ide.eclipse.commons.ui.UiUtil.openUrl;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.springframework.ide.eclipse.wizard.WizardPlugin;
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.content.ContentManager;
import org.springframework.ide.eclipse.wizard.gettingstarted.content.Describable;
import org.springframework.ide.eclipse.wizard.gettingstarted.content.GSContent;
import org.springframework.ide.eclipse.wizard.gettingstarted.content.GettingStartedContent;
import org.springframework.ide.eclipse.wizard.gettingstarted.content.GettingStartedGuide;
import org.springframework.ide.eclipse.wizard.gettingstarted.importing.ImportConfiguration;
import org.springframework.ide.eclipse.wizard.gettingstarted.importing.ImportStrategy;
import org.springframework.ide.eclipse.wizard.gettingstarted.importing.ImportUtils;
import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.UIThreadDownloadDisallowed;
import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression;
import org.springsource.ide.eclipse.commons.livexp.core.LiveSet;
import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable;
import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult;
import org.springsource.ide.eclipse.commons.livexp.core.Validator;
/**
* Core counterpart of <b>GSImportWizard</b> (essentially this is a 'model' for the wizard
* UI.
*
* @author Kris De Volder
*/
public class GSImportWizardModel {
static final ValidationResult isDownloadingMessage(GSContent g) {
return ValidationResult.info(g.getDisplayName()+" is downloading...");
}
public class CodeSetValidator extends LiveExpression<ValidationResult> {
private final LiveVariable<GSContent> codesetProvider;
private final LiveSet<String> selectedNames;
private final LiveExpression<String[]> validCodesetNames;
public CodeSetValidator(LiveVariable<GSContent> guide, LiveSet<String> codesets, LiveExpression<String[]> validCodeSetNames) {
this.codesetProvider = guide;
this.selectedNames = codesets;
this.validCodesetNames = validCodeSetNames;
this.dependsOn(guide);
this.dependsOn(codesets);
this.dependsOn(validCodeSetNames);
}
@Override
protected ValidationResult compute() {
try {
GSContent g = codesetProvider.getValue();
if (g!=null) { //Don't check or produce errors unless a content provider has been selected.
boolean codesetSelected = false;
try {
Set<String> names = selectedNames.getValue();
if (names != null && !names.isEmpty()) {
for (String name : names) {
CodeSet cs = g.getCodeSet(name);
if (cs!=null) {
codesetSelected = true;
ImportConfiguration conf = ImportUtils.importConfig(g, cs);
ValidationResult valid = ImportUtils.validateImportConfiguration(conf);
if (!valid.isOk()) {
return valid;
}
}
}
}
if (!codesetSelected) {
//Selectiong nothing is only allowed if there is in fact nothing to select
//otherwise at least on codeset must be selected for import.
String[] validNames = validCodesetNames.getValue();
if (validNames!=null && validNames.length>0) {
return ValidationResult.error("At least one codeset should be selected");
}
}
} catch (UIThreadDownloadDisallowed e) {
scheduleDownloadJob();
return isDownloadingMessage(g);
}
}
} catch (Throwable e) {
//Unexpected. So log it for more info but also try to create a sensible error message in
// the wizard.
WizardPlugin.log(e);
return ValidationResult.error(ExceptionUtil.getMessage(e));
}
return ValidationResult.OK;
}
}
/**
* ContentManager instance that provides all the content that this wizard can import.
* By default this is content discovered automatically with the default content manager
* instance. However it is possible to set the Content manager to browser / import
* content provided another way.
*/
private ContentManager contentManager = GettingStartedContent.getInstance();
/**
* The chosen guide to import stuff from.
*/
private final LiveVariable<GSContent> guide = new LiveVariable<GSContent>();
/**
* Chosen element in the content picker whether it is an actual GSContent item
* or a ContentType. Used to update description instead of 'c
*/
private final LiveVariable<Object> rawSelection = new LiveVariable<Object>();
/**
* The names of the codesets selected for import.
*/
private final LiveSet<String> codesets = new LiveSet<String>(new HashSet<String>());
{
codesets.addAll(GettingStartedGuide.defaultCodesetNames()); //Select all codesets by default.
}
/**
* The valid codeset names w.r.t. the currently selected guide
*/
public final LiveExpression<String[]> validCodesetNames = new LiveExpression<String[]>(null) {
@Override
protected String[] compute() {
try {
GSContent g = guide.getValue();
if (g!=null) {
List<CodeSet> validSets = g.getCodeSets();
if (validSets!=null) {
String[] names = new String[validSets.size()];
for (int i = 0; i < names.length; i++) {
names[i] = validSets.get(i).getName();
}
return names;
}
}
} catch (UIThreadDownloadDisallowed e) {
//Failed because content is not yet downloade but this is ok...
//just schedule download to happen later and in the mean time return something sensible
scheduleDownloadJob();
} catch (Throwable e) {
WizardPlugin.log(e);
}
return GettingStartedGuide.defaultCodesetNames();
}
};
/**
* The build type chosen by user
*/
private final LiveVariable<BuildType> buildType = new LiveVariable<BuildType>(BuildType.DEFAULT);
private final LiveExpression<ValidationResult> guideValidator = Validator.notNull(guide, "No GS content selected");
private final LiveExpression<ValidationResult> codesetValidator = new CodeSetValidator(guide, codesets, validCodesetNames);
private final LiveExpression<ValidationResult> buildTypeValidator = new Validator() {
@Override
protected ValidationResult compute() {
GSContent g = guide.getValue();
BuildType bt = buildType.getValue();
return validateBuildType(g, bt);
}
};
private ValidationResult validateBuildType(GSContent g, BuildType bt) {
try {
if (g!=null) {
try {
if (bt==null) {
return ValidationResult.error("No build type selected");
} else {
List<String> codesetNames = codesets.getValues();
if (codesetNames!=null) {
for (String csname : codesetNames) {
CodeSet cs = g.getCodeSet(csname);
if (cs!=null) {
ValidationResult result = cs.validateBuildType(bt);
if (!result.isOk()) {
return result.withMessage("CodeSet '"+csname+"': "+result.msg);
}
ImportStrategy importStrategy = bt.getImportStrategy();
if (!importStrategy.isSupported()) {
//This means some required STS component like m2e or gradle tooling is not installed
return ValidationResult.error(bt.getNotInstalledMessage());
}
}
}
}
}
} catch (UIThreadDownloadDisallowed e) {
//Careful... check some of the validation will trigger downloads. This is not allowed in UI thread.
scheduleDownloadJob();
return isDownloadingMessage(g);
}
}
return ValidationResult.OK;
} catch (Throwable e) {
WizardPlugin.log(e);
return ValidationResult.error(ExceptionUtil.getMessage(e));
}
}
public final LiveExpression<Boolean> isDownloaded = new LiveExpression<Boolean>(false) {
@Override
protected Boolean compute() {
GSContent g = guide.getValue();
return g == null || g.isDownloaded();
}
};
/**
* Tries to select a valid build type when a guide is selected.
* The tricky bit is that validity of build types can not be determined until guide
* content has been downloaded locally. At this point if user clicks around rapidly
* another guide may already have been selected. Thus, this code should be run
* any time the guide selection changes as well as any time the download
* status changes.
*/
private final LiveExpression<Void> autoSelectBuildType = new LiveExpression<Void>() {
@Override
protected Void compute() {
GSContent g = guide.getValue();
if (g!=null) {
if (g.isDownloaded()) {
//Yes, we depend on the value of buildType but shouldn't respond to changes on it.
// We do not want to autoselect a buildType when a user selects one. That would be
// mighty annoying.
BuildType bt = buildType.getValue();
if (!validateBuildType(g, bt).isOk()) {
for (BuildType other : BuildType.values()) {
if (other!=bt) {
if (validateBuildType(g,other).isOk()) {
buildType.setValue(other);
}
}
}
}
}
}
return null;
};
}
.dependsOn(isDownloaded)
.dependsOn(guide);
public final LiveExpression<ValidationResult> downloadStatus = new Validator() {
@Override
protected ValidationResult compute() {
GSContent g = guide.getValue();
if (g == null) {
return ValidationResult.OK;
} else {
return ValidationResult.from(g.getZip().getDownloadStatus());
}
}
};
/**
* The description of the current guide.
*/
public final LiveExpression<String> description = new LiveExpression<String>("<no description>") {
@Override
protected String compute() {
Object g = rawSelection.getValue();
if (g!=null && g instanceof Describable) {
return ((Describable) g).getDescription();
}
return "Select Getting Started Content to see its Description";
}
};
public final LiveExpression<URL> homePage = new LiveExpression<URL>(null) {
@Override
protected URL compute() {
GSContent g = guide.getValue();
if (g!=null) {
return g.getHomePage();
}
return null;
}
};
/**
* Indicates whether the user has selected the option to open the home page.
*/
private final LiveVariable<Boolean> enableOpenHomePage = new LiveVariable<Boolean>(true);
{
buildTypeValidator.dependsOn(guide);
buildTypeValidator.dependsOn(isDownloaded);
buildTypeValidator.dependsOn(buildType);
buildTypeValidator.dependsOn(codesets);
isDownloaded.dependsOn(guide);
downloadStatus.dependsOn(guide);
description.dependsOn(rawSelection);
homePage.dependsOn(guide);
validCodesetNames.dependsOn(guide);
validCodesetNames.dependsOn(isDownloaded);
codesetValidator.dependsOn(isDownloaded);
//Note: some other dependsOn are registered inside CodeSetValidator class itself.
// isDownloaded is an exception because is still null when CodeSetValidator class gets
// instantiated.
}
/**
* Downloads currently selected guide content (if it is not already cached locally.
*/
public void performDownload(IProgressMonitor mon) {
mon.beginTask("Downloading", 1);
try {
GSContent g = guide.getValue();
if (g!=null) {
g.getZip().getFile(); //This forces download
}
} catch (Exception e) {
//Don't throw exceptions they are now tracked via downloadStatus.
WizardPlugin.log(e); //Log for more details than downloadStatus message (i.e. stack trace).
} finally {
isDownloaded.refresh();
downloadStatus.refresh();
mon.done();
}
}
private void scheduleDownloadJob() {
Job job = new Job("Downloading guide content") {
@Override
protected IStatus run(IProgressMonitor mon) {
try {
performDownload(mon);
} catch (Throwable e) {
return ExceptionUtil.status(e);
}
return Status.OK_STATUS;
}
};
job.schedule();
}
/**
* Performs the final step of the wizard when user clicks on Finish button.
* @throws InterruptedException
* @throws InvocationTargetException
*/
public boolean performFinish(IProgressMonitor mon) throws InvocationTargetException, InterruptedException {
//The import will be carried out with whatever the currently selected values are
// in all the input fields / variables / widgets.
GSContent g = guide.getValue();
BuildType bt = buildType.getValue();
Set<String> codesetNames = codesets.getValue();
mon.beginTask("Import guide content", codesetNames.size()+1);
try {
for (String name : codesetNames) {
CodeSet cs = g.getCodeSet(name);
if (cs==null) {
//Ignore 'invalid' codesets. This is a bit of a hack so that we can retain selected codeset names
// across guide selection changes. To do that we remember 'selected' cs names even if they
// aren't valid for the current guide. That way the checkbox state stays consistent
// when switching between guides (otherwise 'invalid' names would have to be cleared when switching to
// a guide).
mon.worked(1);
} else {
IRunnableWithProgress oper = bt.getImportStrategy().createOperation(ImportUtils.importConfig(
g,
cs
));
oper.run(new SubProgressMonitor(mon, 1));
}
}
if (enableOpenHomePage.getValue()) {
openHomePage();
}
return true;
} catch (UIThreadDownloadDisallowed e) {
//This shouldn't be possible... Finish button won't be enabled unless all is validated.
//This implies the content has been downloaded (can't be validated otherwise).
WizardPlugin.log(e);
return false;
} finally {
mon.done();
}
}
public void openHomePage() {
URL url = homePage.getValue();
if (url!=null) {
openUrl(url.toString());
}
}
// public void setGuide(GettingStartedGuide guide) {
// this.guide.setValue(guide);
// }
//
// public GettingStartedGuide getGuide() {
// return guide.getValue();
// }
public SelectionModel<BuildType> getBuildTypeModel() {
return new SelectionModel<BuildType>(buildType, buildTypeValidator);
}
public SelectionModel<GSContent> getGSContentSelectionModel() {
return new SelectionModel<GSContent>(guide, guideValidator);
}
public MultiSelectionModel<String> getCodeSetModel() {
return new MultiSelectionModel<String>(codesets, codesetValidator);
}
public LiveExpression<Boolean> isDownloaded() {
return isDownloaded;
}
public LiveVariable<Boolean> getEnableOpenHomePage() {
return enableOpenHomePage;
}
public void setItem(GSContent guide) {
this.guide.setValue(guide);
}
/**
* The 'raw' selection in the UI will be sent here. I.e. selected object will
* be sent whether it is actual content or a ContentTypeNode.
* <p>
* Normally clients shouldn't be interested in this selection. It is used
* to allow the Description section of the UI to display descriptions for
* any selected item, including non-content nodes in the tree.
*/
public LiveVariable<Object> getRawSelection() {
return this.rawSelection;
}
public ContentManager getContentManager() {
return contentManager;
}
public void setContentManager(ContentManager contentManager) {
this.contentManager = contentManager;
}
}