/*******************************************************************************
* 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.workflow;
import org.eclipse.che.plugin.pullrequest.client.events.ContextInvalidatedEvent;
import org.eclipse.che.plugin.pullrequest.client.events.CurrentContextChangedEvent;
import org.eclipse.che.plugin.pullrequest.client.events.StepEvent;
import org.eclipse.che.plugin.pullrequest.client.vcs.VcsServiceProvider;
import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService;
import org.eclipse.che.plugin.pullrequest.shared.dto.Configuration;
import com.google.common.base.Optional;
import com.google.inject.Singleton;
import com.google.web.bindery.event.shared.EventBus;
import org.eclipse.che.api.core.model.project.ProjectConfig;
import org.eclipse.che.api.promises.client.Function;
import org.eclipse.che.api.promises.client.FunctionException;
import org.eclipse.che.api.promises.client.Operation;
import org.eclipse.che.api.promises.client.OperationException;
import org.eclipse.che.api.promises.client.Promise;
import org.eclipse.che.ide.api.app.AppContext;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.util.loging.Log;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.CREATING_PR;
import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.INITIALIZING;
import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.READY_TO_CREATE_PR;
import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.READY_TO_UPDATE_PR;
import static org.eclipse.che.plugin.pullrequest.client.workflow.WorkflowStatus.UPDATING_PR;
import static com.google.common.base.Optional.fromNullable;
/**
* This class is responsible for maintaining the context between the different steps
* and to maintain the state of the contribution workflow.
*
* @author Yevhenii Voevodin
*/
@Singleton
public class WorkflowExecutor {
private final EventBus eventBus;
private final AppContext appContext;
private final VcsServiceProvider vcsServiceProvider;
private final DtoFactory dtoFactory;
private final Map<String, Context> projectNameToContextMap;
private final Map<String, ChainExecutor> projectNameToChainExecutorMap;
private final Map<String, ContributionWorkflow> hostingServiceToWorkflowMap;
@Inject
public WorkflowExecutor(final EventBus eventBus,
final DtoFactory dtoFactory,
final AppContext appContext,
final VcsServiceProvider vcsServiceProvider,
final Map<String, ContributionWorkflow> workflowMap) {
this.eventBus = eventBus;
this.dtoFactory = dtoFactory;
this.appContext = appContext;
this.vcsServiceProvider = vcsServiceProvider;
this.projectNameToContextMap = new HashMap<>();
this.projectNameToChainExecutorMap = new HashMap<>();
this.hostingServiceToWorkflowMap = workflowMap;
}
/**
* Should be invoked when step execution is successful.
*
* @param step
* step which execution is done
* @param context
* execution context
*/
public void done(final Step step, final Context context) {
if (!(step instanceof SyntheticStep)) {
eventBus.fireEvent(new StepEvent(context, step, true));
}
executeNextStep(context);
}
/**
* Should be invoked when step execution is failed.
*
* @param step
* step which execution is failed
* @param context
* execution context
* @param message
* error message
*/
public void fail(final Step step, final Context context, final String message) {
// restore context status from the processing to stable
// the simple implementation of the status spec
Log.error(getClass(), "Exec error " + step.getClass() + ", msg: " + message);
switch (context.getStatus()) {
case INITIALIZING:
invalidateContext(context.getProject());
break;
case CREATING_PR:
context.setStatus(READY_TO_CREATE_PR);
break;
case UPDATING_PR:
context.setStatus(READY_TO_UPDATE_PR);
break;
default:
break;
}
if (!(step instanceof SyntheticStep)) {
eventBus.fireEvent(new StepEvent(context, step, false, message));
}
}
/**
* Initializes {@link ContributionWorkflow} provided by {@code vcsHistingService}.
* If context for such project already initialized then it either invalidates context when
* vcs changes are detected, or fires {@link CurrentContextChangedEvent} otherwise.
*
* @param vcsHostingService
* VCS hosting service based on project origin remote
* @param project
* project for which initialization should be performed
*/
public void init(final VcsHostingService vcsHostingService, final ProjectConfig project) {
final Optional<Context> contextOpt = getContext(project.getName());
if (!contextOpt.isPresent()) {
doInit(vcsHostingService, project);
} else {
checkVcsState(contextOpt.get()).then(new Operation<Boolean>() {
@Override
public void apply(Boolean stateChanged) throws OperationException {
if (stateChanged) {
invalidateContext(contextOpt.get().getProject());
doInit(vcsHostingService, project);
} else {
eventBus.fireEvent(new CurrentContextChangedEvent(contextOpt.get()));
}
}
});
}
}
/**
* Returns an {@link Optional} describing the context for project with name {@code projectName},
* or an empty {@code Optional} if context doesn't exist.
*/
public Optional<Context> getContext(final String projectName) {
return fromNullable(projectNameToContextMap.get(projectName));
}
/** Returns the context based on current project in {@link AppContext}. */
public Context getCurrentContext() {
return getContext(getCurrentProject().getName()).get();
}
/**
* Executes {@link ContributionWorkflow#creationChain(Context)} based on given {@code context}.
*
* @param context
* project for which pull request should be created
*/
public void createPullRequest(final Context context) {
context.setStatus(CREATING_PR);
final StepsChain contributeChain = getWorkflow(context).creationChain(context)
.then(new ChangeContextStatusStep(CREATING_PR, READY_TO_UPDATE_PR));
projectNameToChainExecutorMap.put(context.getProject().getName(), new ChainExecutor(contributeChain));
executeNextStep(context);
}
/**
* Executes {@link ContributionWorkflow#updateChain(Context)} based on given {@code context}.
*
* @param context
* project for which pull request should be updated
*/
public void updatePullRequest(final Context context) {
context.setStatus(UPDATING_PR);
final StepsChain updateChain = getWorkflow(context).updateChain(context)
.then(new ChangeContextStatusStep(UPDATING_PR, READY_TO_UPDATE_PR));
projectNameToChainExecutorMap.put(context.getProject().getName(), new ChainExecutor(updateChain));
executeNextStep(context);
}
/**
* Invalidates context for given {@code project}.
* If any {@link ChainExecutor} exists for context which is invalidated
* then executor will be removed and the next chain step won't be performed.
*
* <p>Fires {@link ContextInvalidatedEvent} if context for given project exists.
*
* @param project
* project for which context should be invalidated
*/
public void invalidateContext(final ProjectConfig project) {
final Optional<Context> contextOpt = getContext(project.getName());
if (contextOpt.isPresent()) {
projectNameToContextMap.remove(project.getName());
projectNameToChainExecutorMap.remove(project.getName());
eventBus.fireEvent(new ContextInvalidatedEvent(contextOpt.get()));
}
}
private void doInit(final VcsHostingService vcsHostingService, final ProjectConfig project) {
final Context context = new Context(eventBus);
context.setVcsHostingService(vcsHostingService);
context.setVcsService(vcsServiceProvider.getVcsService(project));
context.setProject(project);
context.setConfiguration(dtoFactory.createDto(Configuration.class));
context.setStatus(INITIALIZING);
projectNameToContextMap.put(project.getName(), context);
// executes init steps chain for vcs hosting service workflow
final StepsChain initChain = getWorkflow(context).initChain(context)
.then(new ChangeContextStatusStep(INITIALIZING, READY_TO_CREATE_PR));
projectNameToChainExecutorMap.put(project.getName(), new ChainExecutor(initChain));
executeNextStep(context);
}
private ProjectConfig getCurrentProject() {
return appContext.getRootProject();
}
private Promise<Boolean> checkVcsState(final Context context) {
return context.getVcsService()
.getBranchName(context.getProject())
.then(new Function<String, Boolean>() {
@Override
public Boolean apply(String branchName) throws FunctionException {
return !branchName.equals(context.getWorkBranchName());
}
});
}
private ContributionWorkflow getWorkflow(Context context) {
final String serviceName = context.getVcsHostingService().getName();
final ContributionWorkflow strategy = hostingServiceToWorkflowMap.get(serviceName);
if (strategy == null) {
throw new IllegalArgumentException("There is no contribution strategy for the '" + serviceName + "' service");
}
return strategy;
}
private void executeNextStep(final Context context) {
final ChainExecutor executor = getExecutor(context);
if (executor != null) {
executor.execute(this, context);
}
}
private ChainExecutor getExecutor(final Context context) {
return projectNameToChainExecutorMap.get(context.getProject().getName());
}
public static class ChangeContextStatusStep implements Step {
private final WorkflowStatus from;
private final WorkflowStatus to;
public ChangeContextStatusStep(final WorkflowStatus status, final WorkflowStatus to) {
this.from = status;
this.to = to;
}
@Override
public void execute(final WorkflowExecutor executor, final Context context) {
if (context.getStatus() == from) {
context.setStatus(to);
}
executor.done(this, context);
}
}
}