/*
* Copyright (c) 2015 the original author or authors.
* 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:
* Etienne Studer & Donát Csikós (Gradle Inc.) - initial API and implementation and initial documentation
*/
package org.eclipse.buildship.ui.view.execution;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import org.eclipse.buildship.core.GradlePluginsRuntimeException;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.gradle.tooling.Failure;
import org.gradle.tooling.events.FailureResult;
import org.gradle.tooling.events.FinishEvent;
import java.net.URI;
import java.util.List;
/**
* Dialog presenting a list of {@link Failure} instances.
*/
public final class FailureDialog extends Dialog {
private static final String FAILURE_DETAILS_URL_PREFIX = "org.gradle.api.GradleException: There were failing tests. See the report at: "; //$NON-NLS-1$
private final String title;
private final ImmutableList<FailureItem> failureItems;
private Label operationNameText;
private Text messageText;
private Text detailsText;
private Label urlLabel;
private Link urlLink;
private Button backButton;
private Button nextButton;
private Button copyButton;
private Clipboard clipboard;
private int selectionIndex;
public FailureDialog(Shell parent, String title, List<FinishEvent> failureEvents) {
super(parent);
this.title = Preconditions.checkNotNull(title);
this.failureItems = FailureItem.from(failureEvents);
setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL);
}
@Override
protected void configureShell(Shell shell) {
super.configureShell(shell);
shell.setText(this.title);
}
@Override
protected Control createDialogArea(Composite parent) {
Composite container = (Composite) super.createDialogArea(parent);
GridData containerGridData = new GridData(SWT.FILL, SWT.FILL, true, true);
containerGridData.widthHint = convertHorizontalDLUsToPixels(IDialogConstants.MINIMUM_MESSAGE_AREA_WIDTH);
container.setLayoutData(containerGridData);
container.setLayout(new GridLayout(5, false));
Label operationNameLabel = new Label(container, SWT.NONE);
operationNameLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1));
operationNameLabel.setText(ExecutionViewMessages.Dialog_Failure_Operation_Label);
this.operationNameText = new Label(container, SWT.NONE);
GridData operationNameLayoutData = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1);
this.operationNameText.setLayoutData(operationNameLayoutData);
this.backButton = new Button(container, SWT.FLAT | SWT.CENTER);
this.backButton.setToolTipText(ExecutionViewMessages.Dialog_Failure_Back_Tooltip);
this.backButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
this.backButton.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_BACK));
this.nextButton = new Button(container, SWT.FLAT | SWT.CENTER);
this.nextButton.setToolTipText(ExecutionViewMessages.Dialog_Failure_Next_Tooltip);
this.nextButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
this.nextButton.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_FORWARD));
this.copyButton = new Button(container, SWT.FLAT | SWT.CENTER);
this.copyButton.setToolTipText(ExecutionViewMessages.Dialog_Failure_Copy_Details_Tooltip);
this.copyButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
this.copyButton.setImage(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_COPY));
Label messageLabel = new Label(container, SWT.NONE);
messageLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1));
messageLabel.setText(ExecutionViewMessages.Dialog_Failure_Message_Label);
this.messageText = new Text(container, SWT.BORDER);
GridData messageTextLayoutData = new GridData(SWT.FILL, SWT.CENTER, true, false, 4, 1);
this.messageText.setLayoutData(messageTextLayoutData);
this.messageText.setEditable(false);
Label detailsLabel = new Label(container, SWT.NONE);
detailsLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 1, 1));
detailsLabel.setText(ExecutionViewMessages.Dialog_Failure_Details_Label);
this.detailsText = new Text(container, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
GridData detailsTextGridData = new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1);
detailsTextGridData.heightHint = 200;
this.detailsText.setLayoutData(detailsTextGridData);
this.detailsText.setEditable(false);
this.urlLabel = new Label(container, SWT.NONE);
this.urlLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
this.urlLabel.setText(ExecutionViewMessages.Dialog_Failure_Link_Label);
this.urlLink = new Link(container, SWT.NONE);
this.urlLink.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 4, 1));
this.clipboard = new Clipboard(parent.getDisplay());
initSelectionIndex();
initEventListeners();
update();
return container;
}
private void initSelectionIndex() {
this.selectionIndex = this.failureItems.isEmpty() ? -1 : 0;
}
private void initEventListeners() {
this.backButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
FailureDialog.this.selectionIndex--;
update();
}
});
this.nextButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
FailureDialog.this.selectionIndex++;
update();
}
});
this.copyButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
Object[] data = {FailureDialog.this.detailsText.getText()};
Transfer[] dataTypes = {TextTransfer.getInstance()};
FailureDialog.this.clipboard.setContents(data, dataTypes);
}
});
this.urlLink.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
String url = (String) FailureDialog.this.urlLink.getData();
try {
// if there is a browser with the same url then reuse it, otherwise open a new one
IWorkbenchBrowserSupport browserSupport = PlatformUI.getWorkbench().getBrowserSupport();
IWebBrowser browser = browserSupport.createBrowser(IWorkbenchBrowserSupport.AS_EDITOR, url, null, url);
browser.openURL(URI.create(url).toURL());
close();
} catch (Exception e) {
throw new GradlePluginsRuntimeException(String.format("Cannot open browser editor for %s.", url), e);
}
}
});
}
@SuppressWarnings("RedundantTypeArguments")
private void update() {
Optional<FailureItem> failureItem = this.selectionIndex == -1 ? Optional.<FailureItem>absent() : Optional.of(this.failureItems.get(this.selectionIndex));
Optional<Failure> failure = failureItem.isPresent() ? failureItem.get().failure : Optional.<Failure>absent();
this.operationNameText.setText(failureItem.isPresent() ? ExecutionPageNameLabelProvider.renderVerbose(failureItem.get().event) : ""); //$NON-NLS-1$
this.messageText.setText(failure.isPresent() ? Strings.nullToEmpty(failure.get().getMessage()) : ""); //$NON-NLS-1$
this.messageText.setEnabled(failureItem.isPresent());
this.detailsText.setText(failure.isPresent() ? collectDetails(failure.get()) : ""); //$NON-NLS-1$
this.detailsText.setEnabled(failureItem.isPresent());
this.backButton.setEnabled(this.selectionIndex > 0);
this.nextButton.setEnabled(this.selectionIndex < this.failureItems.size() - 1);
this.copyButton.setEnabled(failureItem.isPresent() && failure.isPresent() && failure.get().getDescription() != null);
Optional<String> testReportUrl = findTestReportUrl(failure);
this.urlLabel.setVisible(testReportUrl.isPresent());
this.urlLink.setVisible(testReportUrl.isPresent());
this.urlLink.setText(testReportUrl.isPresent() ? "<a>Test Summary</a>" : "");
this.urlLink.setData(testReportUrl.isPresent() ? testReportUrl.get() : null);
// force redraw since different failures can have different number of lines in the message
this.operationNameText.getParent().layout(true);
}
private Optional<String> findTestReportUrl(Optional<Failure> failure) {
if (failure.isPresent()) {
String description = failure.get().getDescription();
int beginIndex = description.indexOf(FAILURE_DETAILS_URL_PREFIX);
if (beginIndex >= 0) {
int endIndex = description.indexOf('\n', beginIndex);
String url = description.substring(beginIndex + FAILURE_DETAILS_URL_PREFIX.length(), endIndex);
return Optional.of(url);
}
}
return Optional.absent();
}
private String collectDetails(Failure failure) {
return collectDetailsRecursively(failure);
}
private String collectDetailsRecursively(Failure failure) {
StringBuilder result = new StringBuilder();
result.append(Strings.nullToEmpty(failure.getDescription()));
List<? extends Failure> causes = failure.getCauses();
if (!causes.isEmpty()) {
result.append('\n').append(ExecutionViewMessages.Dialog_Failure_Root_Cause_Label).append(' ');
for (Failure cause : causes) {
result.append(collectDetailsRecursively(cause));
}
}
return result.toString();
}
@Override
protected void createButtonsForButtonBar(Composite parent) {
createButton(parent, IDialogConstants.OK_ID, IDialogConstants.CLOSE_LABEL, false);
}
@Override
public boolean close() {
if (this.clipboard != null) {
this.clipboard.dispose();
this.clipboard = null;
}
return super.close();
}
/**
* Represents a failure item shown in the failure dialog. One finish event can have multiple
* failures and so for each failure of each event we show a failure item in the failure dialog.
*/
private static final class FailureItem {
private final FinishEvent event;
private final Optional<Failure> failure;
private FailureItem(FinishEvent event, Optional<Failure> failure) {
this.event = event;
this.failure = failure;
}
private static ImmutableList<FailureItem> from(final FinishEvent event) {
List<? extends Failure> failures = ((FailureResult) event.getResult()).getFailures();
ImmutableList<FailureItem> failureItems = FluentIterable.from(failures).transform(new Function<Failure, FailureItem>() {
@Override
public FailureItem apply(Failure failure) {
return new FailureItem(event, Optional.of(failure));
}
}).toList();
return failureItems.isEmpty() ? ImmutableList.of(new FailureItem(event, Optional.<Failure>absent())) : failureItems;
}
private static ImmutableList<FailureItem> from(List<FinishEvent> events) {
ImmutableList.Builder<FailureItem> failureItems = ImmutableList.builder();
for (FinishEvent event : events) {
failureItems.addAll(from(event));
}
return failureItems.build();
}
}
}