/*******************************************************************************
* Copyright (c) 2011 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is 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:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.openshift.express.internal.ui.wizard.application;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.MultiValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.databinding.fieldassist.ControlDecorationSupport;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.PageChangingEvent;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.statushandlers.StatusManager;
import org.jboss.tools.common.ui.WizardUtils;
import org.jboss.tools.common.ui.databinding.InvertingBooleanConverter;
import org.jboss.tools.common.ui.databinding.ValueBindingBuilder;
import org.jboss.tools.common.ui.ssh.SshPrivateKeysPreferences;
import org.jboss.tools.openshift.common.core.utils.FileUtils;
import org.jboss.tools.openshift.common.core.utils.StringUtils;
import org.jboss.tools.openshift.common.ui.wizard.OkButtonWizardDialog;
import org.jboss.tools.openshift.egit.core.EGitUtils;
import org.jboss.tools.openshift.express.internal.ui.ExpressUIActivator;
import org.jboss.tools.openshift.express.internal.ui.job.LoadKeysJob;
import org.jboss.tools.openshift.express.internal.ui.utils.LinkSelectionAdapter;
import org.jboss.tools.openshift.express.internal.ui.wizard.ssh.ManageSSHKeysWizard;
import org.jboss.tools.openshift.internal.common.core.job.JobChainBuilder;
import org.jboss.tools.openshift.internal.common.ui.utils.UIUtils;
import org.jboss.tools.openshift.internal.common.ui.wizard.AbstractOpenShiftWizardPage;
/**
* @author Andre Dietisheim
* @author Rob Stryker
* @author Xavier Coulon
*
*/
public class GitCloningSettingsWizardPage extends AbstractOpenShiftWizardPage {
private GitCloningSettingsWizardPageModel pageModel;
private IOpenShiftApplicationWizardModel wizardModel;
private Button useDefaultRemoteNameButton;
private Button useDefaultRepoPathButton;
private Text remoteNameText;
private Label remoteNameLabel;
private RepoPathValidationStatusProvider repoPathValidator;
private RemoteNameValidationStatusProvider remoteNameValidator;
private Link sshLink;
public GitCloningSettingsWizardPage(ExpressApplicationWizard wizard,
IOpenShiftApplicationWizardModel wizardModel) {
super(
"Import an existing OpenShift application",
"Configure the cloning settings by specifying the clone destination if you create a new project, and the git remote name if you're using an existing project.",
"Cloning settings", wizard);
this.pageModel = new GitCloningSettingsWizardPageModel(wizardModel);
this.wizardModel = wizardModel;
}
@Override
protected void doCreateControls(Composite parent, DataBindingContext dbc) {
GridLayoutFactory.fillDefaults().applyTo(parent);
Composite cloneGroup = createCloneGroup(parent, dbc);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, false).applyTo(cloneGroup);
Composite filler = new Composite(parent, SWT.NONE);
GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(filler);
}
private Composite createCloneGroup(Composite parent, DataBindingContext dbc) {
Group cloneGroup = new Group(parent, SWT.NONE);
cloneGroup.setText("Cloning settings");
cloneGroup.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
GridLayoutFactory.fillDefaults()
.numColumns(3).equalWidth(false).margins(10, 10).applyTo(cloneGroup);
Composite composite = new Composite(cloneGroup, SWT.NONE);
GridDataFactory.fillDefaults()
.align(SWT.FILL, SWT.FILL).grab(true, true).applyTo(composite);
GridLayoutFactory.fillDefaults()
.numColumns(3).margins(15, 15).applyTo(composite);
// Repo Path Management
this.useDefaultRepoPathButton = new Button(composite, SWT.CHECK);
useDefaultRepoPathButton.setText("Use default clone destination");
useDefaultRepoPathButton.setToolTipText("Uncheck if you want to use a custom location to clone to");
GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).span(3, 1).applyTo(useDefaultRepoPathButton);
Label labelForRepoPath = new Label(composite, SWT.NONE);
labelForRepoPath.setText("Git Clone Destination:");
GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).grab(false, false).indent(10, 0)
.applyTo(labelForRepoPath);
final Text repoPathText = new Text(composite, SWT.BORDER);
GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).align(SWT.FILL, SWT.CENTER).grab(true, false)
.applyTo(repoPathText);
final IObservableValue repoPathObservable = WidgetProperties.text(SWT.Modify).observe(repoPathText);
final IObservableValue repoPathModelObservable =
BeanProperties.value(GitCloningSettingsWizardPageModel.PROPERTY_REPO_PATH).observe(pageModel);
ValueBindingBuilder.bind(repoPathObservable).to(repoPathModelObservable).in(dbc);
Button browseRepoPathButton = new Button(composite, SWT.PUSH);
browseRepoPathButton.setText("Browse...");
GridDataFactory.fillDefaults()
.align(SWT.LEFT, SWT.CENTER).hint(100, SWT.DEFAULT).applyTo(browseRepoPathButton);
browseRepoPathButton.addSelectionListener(onRepoPath());
final IObservableValue isDefaultRepoObservable =
WidgetProperties.selection().observe(useDefaultRepoPathButton);
final IObservableValue useDefaultRepoModelObservable = BeanProperties.value(
GitCloningSettingsWizardPageModel.PROPERTY_USE_DEFAULT_REPO_PATH).observe(pageModel);
ValueBindingBuilder.bind(isDefaultRepoObservable).to(useDefaultRepoModelObservable).in(dbc);
ValueBindingBuilder.bind(WidgetProperties.enabled().observe(repoPathText))
.notUpdating(useDefaultRepoModelObservable).converting(new InvertingBooleanConverter()).in(dbc);
ValueBindingBuilder.bind(WidgetProperties.enabled().observe(browseRepoPathButton))
.notUpdating(useDefaultRepoModelObservable).converting(new InvertingBooleanConverter()).in(dbc);
// move focus to the project location text control when not choosing the
// 'Use default location' option.
UIUtils.focusOnSelection(useDefaultRepoPathButton, repoPathText);
this.repoPathValidator =
new RepoPathValidationStatusProvider(
repoPathObservable
, BeanProperties.value(
GitCloningSettingsWizardPageModel.PROPERTY_APPLICATION_NAME).observe(pageModel)
, BeanProperties.value(
GitCloningSettingsWizardPageModel.PROPERTY_NEW_PROJECT).observe(pageModel));
dbc.addValidationStatusProvider(repoPathValidator);
ControlDecorationSupport.create(repoPathValidator, SWT.LEFT | SWT.TOP);
// Remote Name Management
useDefaultRemoteNameButton = new Button(composite, SWT.CHECK);
useDefaultRemoteNameButton.setText("Use default remote name");
GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).span(3, 1).applyTo(useDefaultRemoteNameButton);
this.remoteNameLabel = new Label(composite, SWT.NONE);
remoteNameLabel.setText("Remote name:");
GridDataFactory.fillDefaults().align(SWT.LEFT, SWT.CENTER).grab(false, false).indent(10, 0)
.applyTo(remoteNameLabel);
remoteNameText = new Text(composite, SWT.BORDER);
GridDataFactory.fillDefaults().span(1, 1).align(SWT.LEFT, SWT.CENTER).align(SWT.FILL, SWT.CENTER)
.grab(true, false).applyTo(remoteNameText);
final IObservableValue remoteNameTextObservable = WidgetProperties.text(SWT.Modify).observe(remoteNameText);
final IObservableValue remoteNameModelObservable =
BeanProperties.value(GitCloningSettingsWizardPageModel.PROPERTY_REMOTE_NAME).observe(pageModel);
ValueBindingBuilder.bind(remoteNameTextObservable).to(remoteNameModelObservable).in(dbc);
final IObservableValue useDefaultRemoteNameModelObservable =
BeanProperties.value(GitCloningSettingsWizardPageModel.PROPERTY_USE_DEFAULT_REMOTE_NAME).observe(
pageModel);
final IObservableValue useDefaultRemoteNameObservable =
WidgetProperties.selection().observe(useDefaultRemoteNameButton);
ValueBindingBuilder
.bind(useDefaultRemoteNameObservable)
.to(useDefaultRemoteNameModelObservable)
.in(dbc);
ValueBindingBuilder
.bind(WidgetProperties.enabled().observe(remoteNameText))
.notUpdating(useDefaultRemoteNameModelObservable).converting(new InvertingBooleanConverter()).in(dbc);
ValueBindingBuilder
.bind(WidgetProperties.enabled().observe(remoteNameLabel))
.notUpdating(useDefaultRemoteNameModelObservable).converting(new InvertingBooleanConverter()).in(dbc);
// move focus to the project name text control when choosing the 'Use an
// existing project' option.
useDefaultRemoteNameButton.addSelectionListener(onDefaultRemoteNameUnchecked());
final IObservableValue projectNameModelObservable =
BeanProperties.value(IOpenShiftApplicationWizardModel.PROP_PROJECT_NAME).observe(wizardModel);
dbc.addValidationStatusProvider(
this.remoteNameValidator =
new RemoteNameValidationStatusProvider(remoteNameTextObservable, projectNameModelObservable));
ControlDecorationSupport.create(remoteNameValidator, SWT.LEFT | SWT.TOP);
this.sshLink = new Link(parent, SWT.NONE);
GridDataFactory.fillDefaults()
.align(SWT.FILL, SWT.CENTER).grab(true, false).indent(10, 0).applyTo(sshLink);
sshLink.addSelectionListener(onSshPrefs("SSH2 Preferences"));
sshLink.addSelectionListener(onManageSSHKeys("SSH Keys wizard", dbc));
// we need a binding to have validation setting wizard validation status
Label dummyLabel = new Label(parent, SWT.None);
dummyLabel.setVisible(false);
GridDataFactory.fillDefaults().exclude(true).applyTo(dummyLabel);
ValueBindingBuilder
.bind(WidgetProperties.text().observe(dummyLabel))
.notUpdating(BeanProperties.value(
GitCloningSettingsWizardPageModel.PROPERTY_HAS_REMOTEKEYS).observe(pageModel))
.validatingAfterGet(new IValidator() {
@Override
public IStatus validate(Object value) {
if (!(value instanceof Boolean)) {
return ValidationStatus.ok();
}
Boolean hasRemoteKeys = (Boolean) value;
if (hasRemoteKeys) {
return ValidationStatus.ok();
} else {
return ValidationStatus
.error("No public keys found in your account. Please use the SSH keys wizard.");
}
}
})
.in(dbc);
refreshHasRemoteKeys(dbc);
return cloneGroup;
}
private SelectionAdapter onDefaultRemoteNameUnchecked() {
return new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
remoteNameText.setFocus();
}
};
}
private SelectionListener onRepoPath() {
return new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
DirectoryDialog dialog = new DirectoryDialog(getShell());
dialog.setText("Git clone location");
dialog.setMessage("Choose the location for git clone...");
dialog.setFilterPath(pageModel.getRepositoryPath());
String repositoryPath = dialog.open();
if (repositoryPath != null) {
pageModel.setRepositoryPath(repositoryPath);
}
}
};
}
private SelectionAdapter onSshPrefs(String text) {
return new LinkSelectionAdapter(text) {
@Override
public void doWidgetSelected(SelectionEvent e) {
SshPrivateKeysPreferences.openPreferencesPage(getShell());
}
};
}
private SelectionAdapter onManageSSHKeys(String text, final DataBindingContext dbc) {
return new LinkSelectionAdapter(text) {
@Override
public void doWidgetSelected(SelectionEvent e) {
WizardDialog manageSSHKeysWizard =
new OkButtonWizardDialog(getShell(), new ManageSSHKeysWizard(pageModel.getConnection()));
if (manageSSHKeysWizard.open() == Dialog.OK) {
refreshHasRemoteKeys(dbc);
}
}
};
}
@Override
protected void onPageActivated(DataBindingContext dbc) {
enableWidgets(pageModel.isNewProject());
repoPathValidator.forceRevalidate();
remoteNameValidator.forceRevalidate();
setSSHLinkText();
if (pageModel.isConnected()) {
refreshHasRemoteKeys(dbc);
}
}
private void setSSHLinkText() {
if (pageModel.isConnected()) {
sshLink.setText("Make sure that you have SSH keys added to your OpenShift account "
+ pageModel.getConnection().getUsername()
+ " via <a>SSH Keys wizard</a> and that the private keys are listed in <a>SSH2 Preferences</a>");
} else {
sshLink.setText("Make sure that you have SSH keys added to your OpenShift account"
+ " via <a>SSH Keys wizard</a> and that the private keys are listed in <a>SSH2 Preferences</a>");
}
sshLink.getParent().layout(true, true);
}
private void refreshHasRemoteKeys(DataBindingContext dbc) {
try {
if (!pageModel.isConnected()) {
return;
}
final LoadKeysJob loadKeysJob = new LoadKeysJob(pageModel.getConnection());
new JobChainBuilder(loadKeysJob).runWhenDone(new UIJob("") {
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
IStatus status = loadKeysJob.getResult();
if(status.equals(Status.OK_STATUS)){
pageModel.setHasRemoteKeys(loadKeysJob.getKeys().size() > 0);
setErrorMessage(null);
return Status.OK_STATUS;
}else{
setErrorMessage(status.getMessage());
return status;
}
}
});
WizardUtils.runInWizard(loadKeysJob, getContainer(), dbc);
} catch (Exception e) {
StatusManager.getManager().handle(
ExpressUIActivator.createErrorStatus("Could not load ssh keys.", e), StatusManager.LOG);
}
}
@Override
protected void onPageWillGetActivated(Direction direction, PageChangingEvent event, DataBindingContext dbc) {
if (direction == Direction.FORWARDS) {
pageModel.reset();
dbc.updateTargets();
}
}
private void enableWidgets(boolean isNewProject) {
if (isNewProject) {
useDefaultRepoPathButton.setEnabled(true);
useDefaultRemoteNameButton.setEnabled(false);
useDefaultRemoteNameButton.setSelection(true);
remoteNameText.setEnabled(false);
remoteNameLabel.setEnabled(false);
} else {
useDefaultRepoPathButton.setEnabled(false);
useDefaultRemoteNameButton.setEnabled(true);
remoteNameText.setEnabled(!useDefaultRemoteNameButton.getSelection());
remoteNameLabel.setEnabled(!useDefaultRemoteNameButton.getSelection());
}
}
/**
* A multivalidator for the repo path. Validates the repo path on behalf of
* the selection to use the default repo path and the repo path value.
*/
class RepoPathValidationStatusProvider extends MultiValidator {
private final IObservableValue repoPathObservable;
private final IObservableValue applicationNameModelObservable;
private final IObservableValue newProjectModelObservable;
public RepoPathValidationStatusProvider(IObservableValue repoPathObservable,
IObservableValue applicationNameModelObservable, IObservableValue newProjectModelObservable) {
this.repoPathObservable = repoPathObservable;
this.applicationNameModelObservable = applicationNameModelObservable;
this.newProjectModelObservable = newProjectModelObservable;
}
// Validator is also be called when application name is set..
@Override
protected IStatus validate() {
final String repoPath = (String) repoPathObservable.getValue();
final String applicationName = (String) applicationNameModelObservable.getValue();
final boolean newProject = (Boolean) newProjectModelObservable.getValue();
if (newProject) {
final IPath repoResourcePath = new Path(repoPath);
if (repoResourcePath.isEmpty()
|| !repoResourcePath.isAbsolute()) {
return ValidationStatus.cancel("You need to provide an absolute path that we'll clone to.");
} else if (!FileUtils.canWrite(repoResourcePath.toOSString())) {
return ValidationStatus.error(
NLS.bind("The location {0} is not writeable.", repoResourcePath.toOSString()));
}
final IPath applicationPath = applicationName != null ?
repoResourcePath.append(new Path(applicationName)) : null;
if (applicationPath != null
&& applicationPath.toFile().exists()) {
return ValidationStatus.error(
NLS.bind("The location \"{0}\" already contains a folder named \"{1}\"",
repoResourcePath.toOSString(), applicationName));
}
}
return ValidationStatus.ok();
}
public void forceRevalidate() {
revalidate();
}
}
/**
* A multi validator that validates the remote name on behalf of the
* selection to use the default remote name and the remote name value.
*/
class RemoteNameValidationStatusProvider extends MultiValidator {
private final IObservableValue remoteNameObservable;
private final IObservableValue projectNameObservable;
public RemoteNameValidationStatusProvider(final IObservableValue remoteNameTextObservable,
final IObservableValue projectNameObservable) {
this.remoteNameObservable = remoteNameTextObservable;
this.projectNameObservable = projectNameObservable;
}
@Override
protected IStatus validate() {
IStatus status = Status.OK_STATUS;
String remoteName = (String) remoteNameObservable.getValue();
String projectName = (String) projectNameObservable.getValue();
// skip the validation if the user wants to create a new project.
// The name and state of the existing project do
// not matter...
if (StringUtils.isEmpty(remoteName)) {
return ExpressUIActivator.createErrorStatus(
"The custom remote name must not be empty.");
} else if (!remoteName.matches("\\S+")) {
return ExpressUIActivator.createErrorStatus(
"The custom remote name must not contain spaces.");
} else if (!pageModel.isNewProject()
&& hasRemoteName(remoteName, getProject(projectName))) {
return ExpressUIActivator.createErrorStatus(NLS.bind(
"The project {0} already has a remote named {1}.", projectName, remoteName));
}
return status;
}
public IProject getProject(final String projectName) {
if (StringUtils.isEmpty(projectName)) {
return null;
}
return ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
}
private boolean hasRemoteName(String remoteName, IProject project) {
try {
if (project == null
|| !project.isAccessible()) {
return false;
}
Repository repository = EGitUtils.getRepository(project);
if (repository == null) {
return false;
}
return EGitUtils.hasRemote(remoteName, repository);
} catch (Exception e) {
ExpressUIActivator.log(ExpressUIActivator.createErrorStatus(e.getMessage(), e));
return false;
}
}
public void forceRevalidate() {
revalidate();
}
}
}