/*******************************************************************************
* Copyright (c) 2012-2015 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.ide.extension.builder.client.build;
import org.eclipse.che.api.builder.BuildStatus;
import org.eclipse.che.api.builder.dto.BuildOptions;
import org.eclipse.che.api.builder.dto.BuildTaskDescriptor;
import org.eclipse.che.api.builder.dto.BuilderMetric;
import org.eclipse.che.api.builder.gwt.client.BuilderServiceClient;
import org.eclipse.che.api.builder.internal.Constants;
import org.eclipse.che.api.core.rest.shared.dto.Link;
import org.eclipse.che.api.project.shared.dto.ProjectDescriptor;
import org.eclipse.che.ide.api.app.AppContext;
import org.eclipse.che.ide.api.build.BuildContext;
import org.eclipse.che.ide.api.editor.EditorAgent;
import org.eclipse.che.ide.api.editor.EditorPartPresenter;
import org.eclipse.che.ide.api.event.ProjectActionEvent;
import org.eclipse.che.ide.api.event.ProjectActionHandler;
import org.eclipse.che.ide.api.notification.Notification;
import org.eclipse.che.ide.api.notification.NotificationManager;
import org.eclipse.che.ide.api.parts.WorkspaceAgent;
import org.eclipse.che.ide.collections.Array;
import org.eclipse.che.ide.dto.DtoFactory;
import org.eclipse.che.ide.extension.builder.client.BuilderExtension;
import org.eclipse.che.ide.extension.builder.client.BuilderLocalizationConstant;
import org.eclipse.che.ide.extension.builder.client.console.BuilderConsolePresenter;
import org.eclipse.che.ide.json.JsonHelper;
import org.eclipse.che.ide.rest.AsyncRequestCallback;
import org.eclipse.che.ide.rest.DtoUnmarshallerFactory;
import org.eclipse.che.ide.ui.dialogs.CancelCallback;
import org.eclipse.che.ide.ui.dialogs.ConfirmCallback;
import org.eclipse.che.ide.ui.dialogs.DialogFactory;
import org.eclipse.che.ide.util.Config;
import org.eclipse.che.ide.util.StringUtils;
import org.eclipse.che.ide.util.loging.Log;
import org.eclipse.che.ide.websocket.MessageBus;
import org.eclipse.che.ide.websocket.WebSocketException;
import org.eclipse.che.ide.websocket.rest.SubscriptionHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.web.bindery.event.shared.EventBus;
import javax.annotation.Nullable;
import java.util.Date;
import java.util.List;
import static org.eclipse.che.ide.api.notification.Notification.Status.FINISHED;
import static org.eclipse.che.ide.api.notification.Notification.Status.PROGRESS;
import static org.eclipse.che.ide.api.notification.Notification.Type.ERROR;
import static org.eclipse.che.ide.api.notification.Notification.Type.INFO;
/**
* Controls building application.
*
* @author Artem Zatsarynnyy
*/
@Singleton
public class BuildController implements Notification.OpenNotificationHandler {
protected final AppContext appContext;
protected final BuilderConsolePresenter console;
protected final BuilderServiceClient service;
protected final BuilderLocalizationConstant constant;
protected final WorkspaceAgent workspaceAgent;
protected final MessageBus messageBus;
protected final NotificationManager notificationManager;
protected final DtoFactory dtoFactory;
protected final DtoUnmarshallerFactory dtoUnmarshallerFactory;
/** Handler for processing Maven build status which is received over WebSocket connection. */
protected SubscriptionHandler<BuildTaskDescriptor> buildStatusHandler;
protected LogMessagesHandler buildOutputHandler;
/** Whether any build is performed now? */
protected boolean isBuildInProgress = false;
protected ProjectDescriptor activeProject;
protected Notification notification;
private BuildContext buildContext;
private final DialogFactory dialogFactory;
/** Descriptor of the last build task. */
private BuildTaskDescriptor lastBuildTaskDescriptor;
private EditorAgent editorAgent;
private String baseUrl;
@Inject
protected BuildController(@Named("restContext") String baseUrl,
EventBus eventBus,
WorkspaceAgent workspaceAgent,
AppContext appContext,
final BuilderConsolePresenter console,
BuilderServiceClient service,
BuilderLocalizationConstant constant,
NotificationManager notificationManager,
DtoFactory dtoFactory,
EditorAgent editorAgent,
DtoUnmarshallerFactory dtoUnmarshallerFactory,
MessageBus messageBus,
BuildContext buildContext,
DialogFactory dialogFactory) {
this.baseUrl = baseUrl;
this.workspaceAgent = workspaceAgent;
this.appContext = appContext;
this.console = console;
this.service = service;
this.constant = constant;
this.notificationManager = notificationManager;
this.dtoFactory = dtoFactory;
this.editorAgent = editorAgent;
this.dtoUnmarshallerFactory = dtoUnmarshallerFactory;
this.messageBus = messageBus;
this.buildContext = buildContext;
this.dialogFactory = dialogFactory;
eventBus.addHandler(ProjectActionEvent.TYPE, new ProjectActionHandler() {
@Override
public void onProjectOpened(ProjectActionEvent event) {
}
@Override
public void onProjectClosed(ProjectActionEvent event) {
console.clear();
console.setCurrentBuilderStatus(BuilderStatus.IDLE);
activeProject = null;
lastBuildTaskDescriptor = null;
}
});
}
@Override
public void onOpenClicked() {
workspaceAgent.setActivePart(console);
}
/**
* Build active project.
*
* @param isUserAction
* points whether the build is started directly by user interaction
*/
public void buildActiveProject(final boolean isUserAction) {
//Save the files before building if necessary
Array<EditorPartPresenter> dirtyEditors = editorAgent.getDirtyEditors();
if (dirtyEditors.isEmpty()) {
buildActiveProject(null, isUserAction);
} else {
dialogFactory.createConfirmDialog(constant.titlePromptSaveFiles(), constant.messagePromptSaveFiles(), new ConfirmCallback() {
@Override
public void accepted() {
editorAgent.saveAll(new AsyncCallback() {
@Override
public void onFailure(Throwable caught) {
Log.error(getClass(), JsonHelper.parseJsonMessage(caught.getMessage()));
}
@Override
public void onSuccess(Object result) {
buildActiveProject(null, isUserAction);
}
});
}
}, new CancelCallback() {
@Override
public void cancelled() {
buildActiveProject(null, isUserAction);
}
}).show();
}
}
/**
* Build active project, specifying options to configure build process.
*
* @param buildOptions
* options to configure build process
* @param isUserAction
* points whether the build is started directly by user interaction
*/
public void buildActiveProject(BuildOptions buildOptions, boolean isUserAction) {
if (isBuildInProgress) {
final String message = constant.buildInProgress(activeProject.getName());
Notification notification = new Notification(message, ERROR);
notificationManager.showNotification(notification);
return;
}
console.clear();
lastBuildTaskDescriptor = null;
buildContext.setBuildTaskDescriptor(null);
activeProject = appContext.getCurrentProject().getProjectDescription();
notification = new Notification(constant.buildStarted(activeProject.getName()), PROGRESS, BuildController.this);
notificationManager.showNotification(notification);
console.setCurrentBuilderStatus(BuilderStatus.IN_PROGRESS);
buildContext.setBuilding(true);
if (isUserAction) {
console.setActive();
}
service.build(activeProject.getPath(),
buildOptions,
new AsyncRequestCallback<BuildTaskDescriptor>(dtoUnmarshallerFactory.newUnmarshaller(BuildTaskDescriptor.class)) {
@Override
protected void onSuccess(BuildTaskDescriptor result) {
lastBuildTaskDescriptor = result;
buildContext.setBuildTaskDescriptor(result);
if (result.getStatus() == BuildStatus.SUCCESSFUL) {
// if project wasn't changed from the last build,
// we get result immediately without re-build
onBuildStatusUpdated(result);
} else {
isBuildInProgress = true;
startCheckingStatus(result);
startCheckingOutput(result);
}
}
@Override
protected void onFailure(Throwable exception) {
notification.setStatus(FINISHED);
notification.setType(ERROR);
notification.setMessage(constant.buildFailed());
console.setCurrentBuilderStatus(BuilderStatus.FAILED);
console.print(JsonHelper.parseJsonMessage(exception.getMessage()));
buildContext.setBuilding(false);
}
}
);
}
private void startCheckingStatus(final BuildTaskDescriptor buildTaskDescriptor) {
buildStatusHandler =
new SubscriptionHandler<BuildTaskDescriptor>(dtoUnmarshallerFactory.newWSUnmarshaller(BuildTaskDescriptor.class)) {
@Override
protected void onMessageReceived(BuildTaskDescriptor result) {
lastBuildTaskDescriptor = result;
buildContext.setBuildTaskDescriptor(result);
onBuildStatusUpdated(result);
}
@Override
protected void onErrorReceived(Throwable exception) {
isBuildInProgress = false;
try {
messageBus.unsubscribe(BuilderExtension.BUILD_STATUS_CHANNEL + buildTaskDescriptor.getTaskId(), this);
Log.error(BuildController.class, exception);
} catch (WebSocketException e) {
Log.error(BuildController.class, e);
}
notification.setType(ERROR);
notification.setStatus(FINISHED);
notification.setMessage(exception.getMessage());
console.setCurrentBuilderStatus(BuilderStatus.FAILED);
buildContext.setBuilding(false);
}
};
try {
messageBus.subscribe(BuilderExtension.BUILD_STATUS_CHANNEL + buildTaskDescriptor.getTaskId(), buildStatusHandler);
} catch (WebSocketException e) {
Log.error(BuildController.class, e);
}
}
private void stopCheckingStatus() {
try {
messageBus.unsubscribe(BuilderExtension.BUILD_STATUS_CHANNEL + lastBuildTaskDescriptor.getTaskId(), buildStatusHandler);
} catch (WebSocketException e) {
Log.error(BuildController.class, e);
}
}
private void startCheckingOutput(BuildTaskDescriptor buildTaskDescriptor) {
buildOutputHandler = new LogMessagesHandler(buildTaskDescriptor, console, messageBus);
try {
messageBus.subscribe(BuilderExtension.BUILD_OUTPUT_CHANNEL + buildTaskDescriptor.getTaskId(), buildOutputHandler);
} catch (WebSocketException e) {
Log.error(BuildController.class, e);
}
}
public void showRunningBuild(BuildTaskDescriptor buildTaskDescriptor, String initialMessage) {
console.setActive();
console.print(initialMessage);
this.startCheckingOutput(buildTaskDescriptor);
}
private void stopCheckingOutput() {
buildOutputHandler.stop();
try {
messageBus.unsubscribe(BuilderExtension.BUILD_OUTPUT_CHANNEL + lastBuildTaskDescriptor.getTaskId(), buildOutputHandler);
} catch (WebSocketException e) {
Log.error(BuildController.class, e);
}
}
/**
* Returns URL to the target folder.
*
* @param descriptor build task descriptor
* @return url to the target folder of the build
*/
private String getTargetFolderURL(BuildTaskDescriptor descriptor) {
String url = Window.Location.getProtocol() + "//"
+ Window.Location.getHost() + baseUrl + "/builder/"
+ Config.getCurrentWorkspace().getId()
+ "/browse/"
+ descriptor.getTaskId()
+ "?path=target";
return "Browse <a href=\"" + url + "\" target=\"_blank\" " +
"style=\"color: #61b7ef;\"" +
"onmouseover=\"this.style.textDecoration='underline';\" " +
"onmouseout=\"this.style.textDecoration='none';\" " +
" ><b>target</b></a> folder of the build";
}
/** Process changing build status. */
private void onBuildStatusUpdated(BuildTaskDescriptor descriptor) {
switch (descriptor.getStatus()) {
case SUCCESSFUL:
isBuildInProgress = false;
stopCheckingStatus();
stopCheckingOutput();
notification.setStatus(FINISHED);
notification.setType(INFO);
notification.setMessage(constant.buildFinished(activeProject.getName()));
console.setCurrentBuilderStatus(BuilderStatus.DONE);
console.print("[INFO] " + notification.getMessage());
console.print("[MAVEN] " + getTargetFolderURL(descriptor));
buildContext.setBuilding(false);
break;
case FAILED:
isBuildInProgress = false;
stopCheckingStatus();
stopCheckingOutput();
notification.setStatus(FINISHED);
notification.setType(ERROR);
notification.setMessage(constant.buildFailed());
console.setCurrentBuilderStatus(BuilderStatus.FAILED);
console.print("[ERROR] " + notification.getMessage());
console.print("[MAVEN] " + getTargetFolderURL(descriptor));
buildContext.setBuilding(false);
break;
case CANCELLED:
isBuildInProgress = false;
stopCheckingStatus();
stopCheckingOutput();
notification.setStatus(FINISHED);
notification.setType(ERROR);
notification.setMessage(constant.buildCanceled(activeProject.getName()));
console.setCurrentBuilderStatus(BuilderStatus.FAILED);
console.print("[ERROR] " + notification.getMessage());
buildContext.setBuilding(false);
break;
}
}
/** Returns link to download result of last build task. */
@Nullable
public String getLastBuildResultURL() {
if (lastBuildTaskDescriptor != null) {
Link downloadResultLink = getLink(lastBuildTaskDescriptor, Constants.LINK_REL_DOWNLOAD_RESULT);
if (downloadResultLink != null)
return downloadResultLink.getHref();
if (lastBuildTaskDescriptor.getStatus().equals(BuildStatus.IN_PROGRESS) ||
lastBuildTaskDescriptor.getStatus().equals(BuildStatus.IN_QUEUE))
return constant.artifactNotReady();
}
return null;
}
/** Returns startTime {@link BuilderMetric}. */
@Nullable
public BuilderMetric getStartedTime() {
if (lastBuildTaskDescriptor != null && lastBuildTaskDescriptor.getCreationTime() > 0) {
Date startDate = new Date(lastBuildTaskDescriptor.getCreationTime());
String startDateFormatted = DateTimeFormat.getFormat("dd/MM/yyyy HH:mm:ss").format(startDate);
return dtoFactory.createDto(BuilderMetric.class).withDescription("Process started at")
.withValue(startDateFormatted);
}
return null;
}
/** Returns waitingTimeLimit {@link BuilderMetric}. */
@Nullable
public BuilderMetric getLastBuildTimeoutThreshold() {
if (lastBuildTaskDescriptor == null) {
return null;
}
BuilderMetric waitingTimeLimit = getBuilderMetric(BuilderMetric.TERMINATION_TIME);
if (waitingTimeLimit != null) {
double terminationTime = NumberFormat.getDecimalFormat().parse(waitingTimeLimit.getValue());
final double terminationTimeout = terminationTime - System.currentTimeMillis();
final String value = StringUtils.timeMlsToHumanReadable((long)terminationTimeout);
return dtoFactory.createDto(BuilderMetric.class).withDescription(waitingTimeLimit.getDescription())
.withName(waitingTimeLimit.getName())
.withValue(value);
}
return null;
}
/** Returns endTime {@link BuilderMetric}. */
@Nullable
public BuilderMetric getLastBuildEndTime() {
BuilderMetric builderMetric = getBuilderMetric(BuilderMetric.END_TIME);
if (builderMetric != null && builderMetric.getValue() != null) {
double stopTimeMs = NumberFormat.getDecimalFormat().parse(builderMetric.getValue());
Date startDate = new Date((long)stopTimeMs);
String stopDateFormatted = DateTimeFormat.getFormat("dd/MM/yyyy HH:mm:ss").format(startDate);
return dtoFactory.createDto(BuilderMetric.class).withDescription(builderMetric.getDescription()).withValue(stopDateFormatted);
}
return null;
}
/** Returns runningTime {@link BuilderMetric}. */
@Nullable
public BuilderMetric getLastBuildRunningTime() {
BuilderMetric builderMetric = getBuilderMetric(BuilderMetric.RUNNING_TIME);
if (builderMetric != null && builderMetric.getValue() != null) {
long mss = (long)NumberFormat.getDecimalFormat().parse(builderMetric.getValue());
long ss = mss / 1000; // The value is the number of seconds (example: 4.000s):
int mm = 0;
if (ss >= 60) {
mm = (int)(ss / 60);
ss = ss % 60;
}
int hh = 0;
if (mm >= 60) {
hh = mm / 60;
mm = mm % 60;
}
StringBuilder value = new StringBuilder();
value.append(hh > 9 ? hh : "0" + hh).append("h:");
value.append(mm > 9 ? mm : "0" + mm).append("m:");
value.append(ss > 9 ? ss : "0" + ss).append("s");
return dtoFactory.createDto(BuilderMetric.class).withName(builderMetric.getName())
.withDescription(builderMetric.getDescription())
.withValue(value.toString());
}
return builderMetric;
}
@Nullable
private BuilderMetric getBuilderMetric(String metricName) {
if (lastBuildTaskDescriptor != null) {
for (BuilderMetric buildStat : lastBuildTaskDescriptor.getBuildStats()) {
if (metricName.equals(buildStat.getName())) {
return buildStat;
}
}
}
return null;
}
/** Returns last build task's status. */
@Nullable
public String getLastBuildStatus() {
if (lastBuildTaskDescriptor != null) {
return lastBuildTaskDescriptor.getStatus().toString();
}
return null;
}
@Nullable
private static Link getLink(BuildTaskDescriptor descriptor, String rel) {
List<Link> links = descriptor.getLinks();
for (Link link : links) {
if (link.getRel().equalsIgnoreCase(rel)) {
return link;
}
}
return null;
}
}