/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* 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:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.pullrequest.client.parts.contribute;
import org.eclipse.che.plugin.pullrequest.client.ContributeMessages;
import org.eclipse.che.plugin.pullrequest.client.events.ContextInvalidatedEvent;
import org.eclipse.che.plugin.pullrequest.client.events.ContextInvalidatedHandler;
import org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeEvent;
import org.eclipse.che.plugin.pullrequest.client.events.ContextPropertyChangeHandler;
import org.eclipse.che.plugin.pullrequest.client.events.CurrentContextChangedEvent;
import org.eclipse.che.plugin.pullrequest.client.events.CurrentContextChangedHandler;
import org.eclipse.che.plugin.pullrequest.client.events.StepEvent;
import org.eclipse.che.plugin.pullrequest.client.events.StepHandler;
import org.eclipse.che.plugin.pullrequest.client.steps.CommitWorkingTreeStep;
import org.eclipse.che.plugin.pullrequest.client.workflow.Context;
import org.eclipse.che.plugin.pullrequest.client.workflow.Step;
import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowExecutor;
import org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.inject.Singleton;
import com.google.web.bindery.event.shared.EventBus;
import org.eclipse.che.api.git.shared.Branch;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.ide.api.app.AppContext;
import org.eclipse.che.ide.api.dialogs.CancelCallback;
import org.eclipse.che.ide.api.dialogs.DialogFactory;
import org.eclipse.che.ide.api.dialogs.InputCallback;
import org.eclipse.che.ide.api.dialogs.InputValidator;
import org.eclipse.che.ide.api.notification.NotificationManager;
import org.eclipse.che.ide.api.parts.WorkspaceAgent;
import org.eclipse.che.ide.api.parts.base.BasePresenter;
import org.eclipse.che.ide.api.resources.Project;
import org.eclipse.che.ide.util.loging.Log;
import javax.inject.Inject;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static com.google.common.base.Strings.nullToEmpty;
import static java.util.Arrays.asList;
import static org.eclipse.che.ide.api.constraints.Constraints.LAST;
import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.FLOAT_MODE;
import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL;
import static org.eclipse.che.ide.api.parts.PartStackType.TOOLING;
/**
* Part for the contribution configuration.
*
* @author Kevin Pollet
*/
@Singleton
public class ContributePartPresenter extends BasePresenter implements ContributePartView.ActionDelegate,
StepHandler,
ContextPropertyChangeHandler,
CurrentContextChangedHandler,
ContextInvalidatedHandler {
private final ContributePartView view;
private final WorkspaceAgent workspaceAgent;
private final ContributeMessages messages;
private final WorkflowExecutor workflowExecutor;
private final AppContext appContext;
private final NotificationManager notificationManager;
private final DialogFactory dialogFactory;
private final Map<String, StagesProvider> stagesProviders;
@Inject
public ContributePartPresenter(final ContributePartView view,
final ContributeMessages messages,
final WorkspaceAgent workspaceAgent,
final EventBus eventBus,
final WorkflowExecutor workflow,
final AppContext appContext,
final NotificationManager notificationManager,
final DialogFactory dialogFactory,
final Map<String, StagesProvider> stagesProviders) {
this.view = view;
this.workspaceAgent = workspaceAgent;
this.workflowExecutor = workflow;
this.messages = messages;
this.appContext = appContext;
this.notificationManager = notificationManager;
this.dialogFactory = dialogFactory;
this.stagesProviders = stagesProviders;
this.view.setDelegate(this);
view.addContributionTitleChangedHandler(new TextChangedHandler() {
@Override
public void onTextChanged(String newText) {
final Context curContext = workflowExecutor.getCurrentContext();
curContext.getViewState().setContributionTitle(newText);
}
});
view.addContributionCommentChangedHandler(new TextChangedHandler() {
@Override
public void onTextChanged(String newText) {
final Context curContext = workflowExecutor.getCurrentContext();
curContext.getViewState().setContributionComment(newText);
}
});
view.addBranchChangedHandler(new TextChangedHandler() {
@Override
public void onTextChanged(String branchName) {
final Context curContext = workflowExecutor.getCurrentContext();
if (!branchName.equals(messages.contributePartConfigureContributionSectionContributionBranchNameCreateNewItemText()) &&
!branchName.equals(curContext.getWorkBranchName())) {
checkoutBranch(curContext, branchName, false);
}
}
});
eventBus.addHandler(StepEvent.TYPE, this);
eventBus.addHandler(ContextPropertyChangeEvent.TYPE, this);
eventBus.addHandler(CurrentContextChangedEvent.TYPE, this);
eventBus.addHandler(ContextInvalidatedEvent.TYPE, this);
}
public void open() {
resetView();
workspaceAgent.openPart(ContributePartPresenter.this, TOOLING, LAST);
}
public void remove() {
workspaceAgent.removePart(ContributePartPresenter.this);
}
@Override
public void onContribute() {
final Context context = workflowExecutor.getCurrentContext();
context.getViewState().setStatusMessage(null);
context.getViewState().resetStages();
updateView(context,
new NewContributionPanelUpdate(),
new StatusMessageUpdate(),
new StatusSectionUpdate());
// Extract configuration values and perform contribution
if (isCurrentContext(context)) {
context.getConfiguration()
.withContributionBranchName(view.getContributionBranchName())
.withContributionComment(view.getContributionComment())
.withContributionTitle(view.getContributionTitle());
}
if (context.isUpdateMode()) {
workflowExecutor.updatePullRequest(context);
} else {
workflowExecutor.createPullRequest(context);
}
updateView(context, new ContributionButtonUpdate(messages));
}
private void restore(final Context context) {
final List<ViewUpdate> updates = new ArrayList<>();
// Repository panel updates
updates.add(new RepositoryUrlUpdate());
updates.add(new ClonedBranchUpdate());
updates.add(new ProjectNameUpdate());
// All the other panels are available only if the mode is different from INITIALIZING
// All the other panels are hidden by the #open method, which is called before initialization
if (context.getStatus() != WorkflowStatus.INITIALIZING) {
// Configuration panel updates
updates.add(new WorkBranchUpdate());
updates.add(new ContributionTitleUpdate());
updates.add(new ContributionCommentUpdate());
// Contribution button update
updates.add(new ContributionButtonUpdate(messages));
// Status panel updates
updates.add(new StatusSectionUpdate());
updates.add(new StatusMessageUpdate());
// New contribution panel updates
updates.add(new NewContributionPanelUpdate());
}
updateView(context, updates);
}
/** Continuously resets the view state as long as current project is not changed. */
private void resetView() {
final Project project = appContext.getRootProject();
final String projectName = project != null ? project.getName() : null;
if (!isCurrentProject(projectName)) return;
view.setRepositoryUrl("");
if (!isCurrentProject(projectName)) return;
view.setContributeToBranch("");
if (!isCurrentProject(projectName)) return;
view.setContributionBranchName("");
if (!isCurrentProject(projectName)) return;
view.setContributionBranchNameEnabled(true);
if (!isCurrentProject(projectName)) return;
view.setContributionBranchNameList(Collections.<String>emptyList());
if (!isCurrentProject(projectName)) return;
view.setContributionTitle("");
if (!isCurrentProject(projectName)) return;
view.setProjectName("");
if (!isCurrentProject(projectName)) return;
view.setContributionTitleEnabled(true);
if (!isCurrentProject(projectName)) return;
view.setContributionComment("");
if (!isCurrentProject(projectName)) return;
view.setContributionCommentEnabled(true);
if (!isCurrentProject(projectName)) return;
view.setContributeButtonText(messages.contributePartConfigureContributionSectionButtonContributeText());
if (!isCurrentProject(projectName)) return;
view.hideStatusSection();
if (!isCurrentProject(projectName)) return;
view.hideNewContributionSection();
if (!isCurrentProject(projectName)) return;
updateControls();
}
@Override
public void onOpenPullRequestOnVcsHost() {
final Context context = workflowExecutor.getCurrentContext();
Window.open(context.getVcsHostingService().makePullRequestUrl(context.getUpstreamRepositoryOwner(),
context.getUpstreamRepositoryName(),
context.getPullRequestIssueNumber()), "", "");
}
@Override
public void onNewContribution() {
final Context context = workflowExecutor.getCurrentContext();
context.getVcsService().checkoutBranch(context.getProject(), context.getContributeToBranchName(),
false, new AsyncCallback<String>() {
@Override
public void onFailure(final Throwable exception) {
notificationManager.notify(exception.getMessage(), FAIL, FLOAT_MODE);
}
@Override
public void onSuccess(final String branchName) {
resetView();
workflowExecutor.invalidateContext(context.getProject());
workflowExecutor.init(context.getVcsHostingService(), context.getProject());
}
});
}
@Override
public void onRefreshContributionBranchNameList() {
updateView(workflowExecutor.getCurrentContext(), new WorkBranchUpdate());
}
@Override
public void onCreateNewBranch() {
final Context context = workflowExecutor.getCurrentContext();
dialogFactory.createInputDialog(messages.contributePartConfigureContributionDialogNewBranchTitle(),
messages.contributePartConfigureContributionDialogNewBranchLabel(),
new CreateNewBranchCallback(context),
new CancelNewBranchCallback(context))
.withValidator(new BranchNameValidator())
.show();
}
@Override
public void updateControls() {
final String contributionTitle = view.getContributionTitle();
boolean isValid = true;
view.showContributionTitleError(false);
if (contributionTitle == null || contributionTitle.trim().isEmpty()) {
view.showContributionTitleError(true);
isValid = false;
}
view.setContributeButtonEnabled(isValid);
}
@Override
public void go(final AcceptsOneWidget container) {
container.setWidget(view.asWidget());
}
@NotNull
@Override
public String getTitle() {
return messages.contributePartTitle();
}
@Override
public IsWidget getView() {
return view;
}
@Nullable
@Override
public String getTitleToolTip() {
return null;
}
@Override
public int getSize() {
return 350;
}
@Override
public void onStepDone(final StepEvent event) {
final Class<? extends Step> stepClass = event.getStep().getClass();
final Context context = event.getContext();
// if it is necessarily to display stages on this step
if (getProvider(context).getDisplayStagesType(context) == stepClass) {
context.getViewState().setStages(getProvidedStages(context));
updateView(context, new StatusSectionUpdate());
}
// if current step is in list of provided stages types
// then this step is done and view should be affected
if (!context.getViewState().getStages().isEmpty() && getProvidedStepDoneTypes(context).contains(stepClass)) {
context.getViewState().setStageDone(true);
updateView(context, new DisplayCurrentStepResultUpdate(true));
} else if (stepClass == WorkflowExecutor.ChangeContextStatusStep.class) {
if (context.getStatus() == WorkflowStatus.READY_TO_UPDATE_PR) {
final List<ViewUpdate> updates = new ArrayList<>();
// Display status message
final String message;
if (context.getPreviousStatus() == WorkflowStatus.CREATING_PR) {
message = messages.contributePartStatusSectionContributionCreatedMessage();
} else {
message = messages.contributePartStatusSectionContributionUpdatedMessage();
}
context.getViewState().setStatusMessage(message, false);
updates.add(new StatusMessageUpdate());
// Contribution button
updates.add(new ContributionButtonUpdate(messages));
// Config panel
updates.add(new ContributionTitleUpdate());
updates.add(new ContributionCommentUpdate());
// New contribution panel
updates.add(new NewContributionPanelUpdate());
updateView(context, updates);
}
}
}
@Override
public void onStepError(final StepEvent event) {
final Step step = event.getStep();
final Class<? extends Step> stepClass = step.getClass();
final Context context = event.getContext();
if (stepClass == CommitWorkingTreeStep.class) {
if (!context.isUpdateMode()) {
context.getViewState().resetStages();
context.getViewState().setStatusMessage(null);
updateView(context,
new StatusSectionUpdate(),
new StatusMessageUpdate(),
new NewContributionPanelUpdate(),
new ContributionButtonUpdate(messages));
} else {
context.getViewState().resetStages();
context.getViewState().setStatusMessage(event.getMessage(), true);
updateView(context,
new StatusSectionUpdate(),
new StatusMessageUpdate(),
new ContributionButtonUpdate(messages));
}
} else if (getProvidedStepErrorTypes(context).contains(stepClass)) {
context.getViewState().setStageDone(false);
context.getViewState().setStatusMessage(event.getMessage(), true);
updateView(context,
new DisplayCurrentStepResultUpdate(false),
new StatusMessageUpdate(),
new ContributionButtonUpdate(messages));
} else {
context.getViewState().resetStages();
restore(context);
Log.error(ContributePartPresenter.class, "Step error: ", event.getMessage());
}
}
@Override
public void onContextPropertyChange(final ContextPropertyChangeEvent event) {
final Context context = event.getContext();
switch (event.getContextProperty()) {
case CONTRIBUTE_TO_BRANCH_NAME:
updateView(context, new ClonedBranchUpdate());
break;
case WORK_BRANCH_NAME:
updateView(context, new WorkBranchUpdate());
break;
case ORIGIN_REPOSITORY_NAME:
case ORIGIN_REPOSITORY_OWNER:
updateView(context, new RepositoryUrlUpdate());
break;
case PROJECT:
updateView(context, new ProjectNameUpdate());
break;
default:
// nothing to do
break;
}
}
@Override
public void onContextChanged(final Context context) {
restore(context);
}
private void updateView(final Context context, final ViewUpdate... updates) {
updateView(context, asList(updates));
}
private void updateView(final Context context, final List<ViewUpdate> updates) {
for (Iterator<ViewUpdate> it = updates.iterator(); it.hasNext() && isCurrentContext(context); ) {
it.next().update(view, context);
}
}
private void updateView(final Context context, final ViewUpdate update) {
if (isCurrentContext(context)) {
update.update(view, context);
}
}
@Override
public void onContextInvalidated(Context context) {
resetView();
}
/**
* Defines a single update operation.
* Single update operations are required as view is shared between multiple projects.
* If context is switched view should not be updated with the updates related to the previous context.
*/
private interface ViewUpdate {
void update(final ContributePartView view, final Context context);
}
private static class DisplayCurrentStepResultUpdate implements ViewUpdate {
private final boolean result;
public DisplayCurrentStepResultUpdate(boolean result) {
this.result = result;
}
@Override
public void update(final ContributePartView view, final Context context) {
view.setCurrentStatusStepStatus(result);
}
}
private static class NewContributionPanelUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
view.hideNewContributionSection();
if (context.isUpdateMode()) {
view.showNewContributionSection(context.getVcsHostingService().getName());
}
}
}
private static class StatusMessageUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
view.hideStatusSectionMessage();
final Context.ViewState.StatusMessage statusMessage = context.getViewState().getStatusMessage();
if (statusMessage != null) {
view.showStatusSectionMessage(statusMessage.getMessage(), statusMessage.isError());
}
}
}
private static class StatusSectionUpdate implements ViewUpdate {
@Override
public void update(ContributePartView view, Context context) {
view.hideStatusSection();
final List<Context.ViewState.Stage> stepStatuses = context.getViewState().getStages();
if (stepStatuses.size() > 0) {
final String[] names = context.getViewState().getStageNames().toArray(new String[stepStatuses.size()]);
view.showStatusSection(names);
for (Boolean stepStatus : context.getViewState().getStageValues()) {
if (stepStatus == null) {
break;
}
view.setCurrentStatusStepStatus(stepStatus);
}
} else if (context.getViewState().getStatusMessage() != null) {
view.showStatusSection();
}
}
}
private static class ClonedBranchUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
view.setContributeToBranch(nullToEmpty(context.getContributeToBranchName()));
}
}
private static class ContributionButtonUpdate implements ViewUpdate {
private final ContributeMessages messages;
private ContributionButtonUpdate(final ContributeMessages messages) {
this.messages = messages;
}
@Override
public void update(final ContributePartView view, final Context context) {
final boolean isEnabled = !nullToEmpty(context.getViewState().getContributionTitle()).isEmpty();
final boolean isInProgress;
final String buttonText;
switch (context.getStatus()) {
case UPDATING_PR:
buttonText = messages.contributePartConfigureContributionSectionButtonContributeUpdateText();
isInProgress = true;
break;
case READY_TO_UPDATE_PR:
buttonText = messages.contributePartConfigureContributionSectionButtonContributeUpdateText();
isInProgress = false;
break;
case CREATING_PR:
buttonText = messages.contributePartConfigureContributionSectionButtonContributeText();
isInProgress = true;
break;
case READY_TO_CREATE_PR:
buttonText = messages.contributePartConfigureContributionSectionButtonContributeText();
isInProgress = false;
break;
default:
throw new IllegalStateException("Illegal workflow status " + context.getStatus());
}
view.setContributeButtonText(buttonText);
view.setContributionProgressState(isInProgress);
view.setContributeButtonEnabled(isEnabled);
}
}
private class ContributionCommentUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
view.setContributionComment(nullToEmpty(context.getViewState().getContributionComment()));
view.setContributionCommentEnabled(context.getStatus() == WorkflowStatus.READY_TO_CREATE_PR);
}
}
private static class ContributionTitleUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
view.setContributionTitle(nullToEmpty(context.getViewState().getContributionTitle()));
view.setContributionTitleEnabled(context.getStatus() == WorkflowStatus.READY_TO_CREATE_PR);
}
}
private static class RepositoryUrlUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
final String originRepositoryName = context.getOriginRepositoryName();
final String originRepositoryOwner = context.getOriginRepositoryOwner();
if (originRepositoryName != null && originRepositoryOwner != null) {
view.setRepositoryUrl(context.getVcsHostingService()
.makeHttpRemoteUrl(originRepositoryOwner, originRepositoryName));
}
}
}
private static class WorkBranchUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
context.getVcsService()
.listLocalBranches(context.getProject(), new AsyncCallback<List<Branch>>() {
@Override
public void onFailure(final Throwable notUsed) {
}
@Override
public void onSuccess(final List<Branch> branches) {
final List<String> branchNames = new ArrayList<>();
for (final Branch oneBranch : branches) {
branchNames.add(oneBranch.getDisplayName());
}
view.setContributionBranchNameList(branchNames);
view.setContributionBranchName(context.getWorkBranchName());
}
});
}
}
private static class ProjectNameUpdate implements ViewUpdate {
@Override
public void update(final ContributePartView view, final Context context) {
view.setProjectName(context.getProject().getName());
}
}
private boolean isCurrentContext(final Context context) {
final Project project = appContext.getRootProject();
return project != null && Objects.equals(context.getProject().getName(), project.getName());
}
private boolean isCurrentProject(final String projectName) {
final Project project = appContext.getRootProject();
return project != null && Objects.equals(projectName, project.getName());
}
private StagesProvider getProvider(final Context context) {
for (Map.Entry<String, StagesProvider> entry : stagesProviders.entrySet()) {
if (entry.getKey().equals(context.getVcsHostingService().getName())) {
return entry.getValue();
}
}
throw new IllegalStateException("StagesProvider for VCS hosting service "
+ context.getVcsHostingService().getName()
+ " isn't registered");
}
private List<String> getProvidedStages(final Context context) {
return getProvider(context).getStages(context);
}
private Set<Class<? extends Step>> getProvidedStepDoneTypes(final Context context) {
return getProvider(context).getStepDoneTypes(context);
}
private Set<Class<? extends Step>> getProvidedStepErrorTypes(final Context context) {
return getProvider(context).getStepErrorTypes(context);
}
private static class BranchNameValidator implements InputValidator {
private static final Violation ERROR_WITH_NO_MESSAGE = new InputValidator.Violation() {
@Nullable
@Override
public String getMessage() {
return "";
}
@Nullable
@Override
public String getCorrectedValue() {
return null;
}
};
@Nullable
@Override
public Violation validate(final String branchName) {
return branchName.matches("[0-9A-Za-z-]+") ? null : ERROR_WITH_NO_MESSAGE;
}
}
private class CreateNewBranchCallback implements InputCallback {
private final Context context;
public CreateNewBranchCallback(final Context context) {
this.context = context;
}
@Override
public void accepted(final String branchName) {
context.getVcsService()
.isLocalBranchWithName(context.getProject(), branchName, new AsyncCallback<Boolean>() {
@Override
public void onFailure(final Throwable exception) {
notificationManager.notify(exception.getMessage(), FAIL, FLOAT_MODE);
}
@Override
public void onSuccess(final Boolean branchExists) {
if (branchExists) {
notificationManager
.notify(messages.contributePartConfigureContributionDialogNewBranchErrorBranchExists(branchName),
FAIL,
FLOAT_MODE);
} else {
checkoutBranch(context, branchName, true);
}
}
});
}
}
private void checkoutBranch(final Context context, final String branchName, final boolean createNew) {
context.getVcsService()
.checkoutBranch(context.getProject(),
branchName,
createNew,
new AsyncCallback<String>() {
@Override
public void onFailure(final Throwable exception) {
notificationManager.notify(exception.getLocalizedMessage(), FAIL, FLOAT_MODE);
}
@Override
public void onSuccess(final String notUsed) {
workflowExecutor.invalidateContext(context.getProject());
workflowExecutor.init(context.getVcsHostingService(), context.getProject());
}
});
}
private class CancelNewBranchCallback implements CancelCallback {
private final Context context;
private CancelNewBranchCallback(final Context context) {
this.context = context;
}
@Override
public void cancelled() {
updateView(context, new WorkBranchUpdate());
}
}
}