/* * 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); } } }