/*
* 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:
* Simon Scholz (vogella GmbH) - initial API and implementation and initial documentation
*/
package org.eclipse.buildship.ui.view.execution;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import org.gradle.tooling.events.OperationDescriptor;
import org.gradle.tooling.events.task.TaskOperationDescriptor;
import org.gradle.tooling.events.test.JvmTestOperationDescriptor;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import com.gradleware.tooling.toolingmodel.OmniEclipseProject;
import com.gradleware.tooling.toolingmodel.Path;
import com.gradleware.tooling.toolingmodel.repository.FetchStrategy;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.FindReplaceDocumentAdapter;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.TextFileDocumentProvider;
import org.eclipse.buildship.core.CorePlugin;
import org.eclipse.buildship.core.configuration.BuildConfiguration;
import org.eclipse.buildship.core.configuration.RunConfiguration;
import org.eclipse.buildship.core.util.progress.ToolingApiWorkspaceJob;
import org.eclipse.buildship.core.workspace.ModelProvider;
import org.eclipse.buildship.ui.UiPlugin;
import org.eclipse.buildship.ui.util.editor.EditorUtils;
/**
* Opens the test source files for the given
* {@link org.eclipse.buildship.ui.view.execution.OperationItem} test nodes. Knows how to handle
* both Java and Groovy test source files.
*/
public final class OpenTestSourceFileJob extends ToolingApiWorkspaceJob {
private final ImmutableList<OperationItem> operationItems;
private final RunConfiguration runConfig;
public OpenTestSourceFileJob(List<OperationItem> operationItems, RunConfiguration runConfig) {
super("Opening test source files");
this.operationItems = ImmutableList.copyOf(operationItems);
this.runConfig = Preconditions.checkNotNull(runConfig);
}
@Override
protected void runToolingApiJobInWorkspace(IProgressMonitor monitor) throws Exception {
SubMonitor subMonitor = SubMonitor.convert(monitor, this.operationItems.size());
for (OperationItem operationItem : this.operationItems) {
if (monitor.isCanceled()) {
throw new OperationCanceledException();
} else {
searchForTestSource(operationItem, subMonitor.newChild(1));
}
}
}
private void searchForTestSource(OperationItem operationItem, SubMonitor monitor) throws CoreException {
OperationDescriptor operationDescriptor = (OperationDescriptor) operationItem.getAdapter(OperationDescriptor.class);
if (operationDescriptor instanceof JvmTestOperationDescriptor) {
JvmTestOperationDescriptor testOperationDescriptor = (JvmTestOperationDescriptor) operationDescriptor;
String className = testOperationDescriptor.getClassName();
Optional<Path> projectPath = findProjectPath(operationDescriptor);
if (className != null && projectPath.isPresent()) {
String methodName = testOperationDescriptor.getMethodName();
searchForTestSource(className, methodName, projectPath.get(), monitor);
}
}
}
private Optional<Path> findProjectPath(OperationDescriptor operationDescriptor) {
OperationDescriptor parent = operationDescriptor.getParent();
if (parent != null) {
if (parent instanceof TaskOperationDescriptor) {
Path taskPath = Path.from(((TaskOperationDescriptor) parent).getTaskPath());
return Optional.of(taskPath.dropLastSegment());
} else {
return findProjectPath(parent);
}
}
return Optional.absent();
}
private void searchForTestSource(String className, String methodName, Path projectPath, SubMonitor monitor) throws CoreException {
monitor.setTaskName(String.format("Open test source file for class %s.", className));
monitor.setWorkRemaining(2);
List<IProject> project = findProjectContainingTest(projectPath, monitor);
boolean found = searchForJavaTest(className, methodName, project, monitor.newChild(1));
if (!found) {
searchForGroovyTest(className, methodName, project, monitor.newChild(1));
}
}
private boolean searchForJavaTest(String className, String methodName, List<IProject> projects, IProgressMonitor monitor) throws CoreException {
SearchEngine searchEngine = new SearchEngine();
SearchPattern pattern = SearchPattern.createPattern(className, IJavaSearchConstants.TYPE, IJavaSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH);
ShowTestSourceFileSearchRequester requester = new ShowTestSourceFileSearchRequester(methodName);
searchEngine.search(pattern, new SearchParticipant[]{ SearchEngine.getDefaultSearchParticipant() }, createSearchScope(projects, monitor), requester, monitor);
return requester.isFoundJavaTestSourceFile();
}
private void searchForGroovyTest(String className, String methodName, List<IProject> projects, SubMonitor monitor) throws CoreException {
ShowTestSourceFileResourceVisitor visitor = new ShowTestSourceFileResourceVisitor(methodName, className, ImmutableList.of("groovy")); //$NON-NLS-1$
if (projects.isEmpty()) {
ResourcesPlugin.getWorkspace().getRoot().accept(visitor);
} else {
for (IProject project : projects) {
project.accept(visitor);
}
}
}
private List<IProject> findProjectContainingTest(Path projectPath, IProgressMonitor monitor) {
File workingDir = this.runConfig.getBuildConfiguration().getRootProjectDirectory();
Optional<IProject> project = CorePlugin.workspaceOperations().findProjectByLocation(workingDir);
if (!project.isPresent()) {
return Collections.emptyList();
}
BuildConfiguration buildConfig = CorePlugin.configurationManager().loadProjectConfiguration(project.get()).getBuildConfiguration();
ModelProvider modelProvider = CorePlugin.gradleWorkspaceManager().getGradleBuild(buildConfig).getModelProvider();
Set<OmniEclipseProject> eclipseProjects = modelProvider.fetchEclipseGradleProjects(FetchStrategy.LOAD_IF_NOT_CACHED, getToken(), monitor);
List<IProject> result = new ArrayList<IProject>();
for (OmniEclipseProject eclipseProject : eclipseProjects) {
if (eclipseProject.getPath().equals(projectPath)) {
Optional<IProject> workspaceProject = CorePlugin.workspaceOperations().findProjectByName(eclipseProject.getName());
if (workspaceProject.isPresent()) {
result.add(workspaceProject.get());
}
}
}
return result;
}
private IJavaSearchScope createSearchScope(List<IProject> projects, IProgressMonitor monitor) throws CoreException {
List<IJavaProject> javaProjects = new ArrayList<IJavaProject>();
for (IProject project : projects) {
if (project.isAccessible() && project.hasNature(JavaCore.NATURE_ID)) {
javaProjects.add(JavaCore.create(project));
}
}
if (javaProjects.isEmpty()) {
return SearchEngine.createWorkspaceScope();
} else {
return SearchEngine.createJavaSearchScope(javaProjects.toArray(new IJavaProject[0]));
}
}
/**
* Match the type and potentially also the method name.
*/
private final class ShowTestSourceFileSearchRequester extends SearchRequestor {
private final String methodName;
private final AtomicBoolean foundJavaTestSourceFile;
private ShowTestSourceFileSearchRequester(String methodName) {
this.methodName = methodName;
this.foundJavaTestSourceFile = new AtomicBoolean(false);
}
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
if (match.getElement() instanceof IType) {
this.foundJavaTestSourceFile.set(true);
IType classElement = (IType) match.getElement();
IJavaElement methodElement = findMethod(this.methodName, classElement);
openInEditor(methodElement != null ? methodElement : classElement);
}
}
private IJavaElement findMethod(String methodName, IType type) {
// abort search for invalid method names
@SuppressWarnings("restriction")
IStatus status = org.eclipse.jdt.internal.corext.util.JavaConventionsUtil.validateMethodName(methodName, type);
if (!status.isOK()) {
return null;
}
// find parameter-less method by name
IMethod method = type.getMethod(methodName, new String[0]);
if (method != null && method.exists()) {
return method;
}
// search textually by name (for custom runner with test methods having parameters)
try {
for (IMethod methodItem : type.getMethods()) {
if (methodItem.getElementName().equals(methodName)) {
return methodItem;
}
}
return null;
} catch (JavaModelException e) {
// ignore and treat as no method being found
return null;
}
}
private void openInEditor(final IJavaElement javaElement) {
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
@Override
public void run() {
try {
JavaUI.openInEditor(javaElement);
} catch (Exception e) {
String message = String.format("Cannot open Java element %s in editor.", javaElement);
UiPlugin.logger().error(message, e);
}
}
});
}
private boolean isFoundJavaTestSourceFile() {
return this.foundJavaTestSourceFile.get();
}
}
/**
* Find the file for the given class name as a resource in the workspace.
*/
private static final class ShowTestSourceFileResourceVisitor implements IResourceVisitor {
private static final String BIN_FOLDER_NAME = "bin"; //$NON-NLS-1$
private final String methodName;
private final String className;
private final ImmutableList<String> fileExtensions;
private ShowTestSourceFileResourceVisitor(String methodName, String className, List<String> fileExtensions) {
this.methodName = methodName;
this.className = Preconditions.checkNotNull(className);
this.fileExtensions = ImmutableList.copyOf(fileExtensions);
}
@Override
public boolean visit(final IResource resource) throws CoreException {
// short-circuit if the resource is not a file with one of the requested extensions
if (resource.getType() != IResource.FILE || !this.fileExtensions.contains(resource.getFileExtension())) {
return true;
}
// prepare to compare package path of the requested class name with the project path of
// the given resource
String classNameToPath = this.className.replaceAll(Pattern.quote("."), "/"); //$NON-NLS-1$ //$NON-NLS-2$
String projectRelativePath = resource.getProjectRelativePath().toString();
// short-circuit if the resource is in the bin folder or if the paths do not match
if (projectRelativePath.startsWith(BIN_FOLDER_NAME) || !projectRelativePath.contains(classNameToPath)) {
return true;
}
// short-circuit if the resource does not map to a file
@SuppressWarnings({ "cast", "RedundantCast" })
final IFile file = (IFile) resource.getAdapter(IFile.class);
if (file == null) {
return true;
}
// open the requested class and optionally mark the requested method
Display display = PlatformUI.getWorkbench().getDisplay();
display.syncExec(new Runnable() {
@Override
public void run() {
IEditorPart editor = EditorUtils.openInInternalEditor(file, true);
IRegion region = getClassOrMethodRegion(file);
if (region != null) {
EditorUtils.selectAndReveal(region.getOffset(), region.getLength(), editor, file);
}
}
});
return false;
}
private org.eclipse.jface.text.IRegion getClassOrMethodRegion(IFile file) {
// if no method name is available find the class name
if (this.methodName == null) {
try {
FindReplaceDocumentAdapter documentAdapter = createFindReplaceDocumentAdapter(file);
return find(documentAdapter, Files.getNameWithoutExtension(file.getName()));
} catch (Exception e) {
// ignore and treat as no method being found
return null;
}
}
// try to find method name and fall back to class name if method name cannot be found
try {
FindReplaceDocumentAdapter documentAdapter = createFindReplaceDocumentAdapter(file);
IRegion region = find(documentAdapter, this.methodName);
if (region == null) {
documentAdapter = createFindReplaceDocumentAdapter(file);
return find(documentAdapter, Files.getNameWithoutExtension(file.getName()));
}
return region;
} catch (Exception e) {
// ignore and treat as no method being found
return null;
}
}
private FindReplaceDocumentAdapter createFindReplaceDocumentAdapter(IFile file) throws CoreException {
TextFileDocumentProvider textFileDocumentProvider = new TextFileDocumentProvider();
textFileDocumentProvider.connect(file);
IDocument document = textFileDocumentProvider.getDocument(file);
return new FindReplaceDocumentAdapter(document);
}
private IRegion find(FindReplaceDocumentAdapter findReplaceDocumentAdapter, String findString) throws BadLocationException {
return findReplaceDocumentAdapter.find(0, findString, true, true, false, false);
}
}
}