/*******************************************************************************
* Copyright (c) 2016 Pivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springsource.ide.eclipse.commons.frameworks.core.async;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
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.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
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.springsource.ide.eclipse.commons.frameworks.core.FrameworkCoreActivator;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
import reactor.core.publisher.Flux;
import reactor.core.publisher.ReplayProcessor;
import reactor.util.concurrent.QueueSupplier;
/**
* Helper class to perform a search using Eclipse JDT search engine returning
* the search results as a Flux.
* <p>
* The conversion from Eclipse callback style using {@link SearchRequestor} involves
* a buffer that allows subscribers to attach to the Flux after the search has already
* started without loosing results. However, if the buffer overflows then results will
* be lost.
* <p>
* Clients should therfore start consuming the results as soon as possible and avoid
* blocking the pipeline to avoid the loss of results they may care about.
* <p>
* Alternatively, client can specify a large enough buffer size so that the buffer can hold
* at least as many results as the client may care to retrieve. This will allow the returned
* Flux to be reused any number of times without timing constraints, provided that the
* consumer never requests more than the number of buffered results.
*
* @author Kris De Volder
*/
public class FluxJdtSearch {
private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder");
private static void debug(String string) {
if (DEBUG) {
System.out.println(string);
}
}
private SearchEngine engine = new SearchEngine();
private IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
private SearchPattern pattern = null;
private SearchParticipant[] participants = new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()};
private int bufferSize = QueueSupplier.SMALL_BUFFER_SIZE;
private boolean useSystemJob = false;
private int jobPriority = Job.INTERACTIVE;
public FluxJdtSearch engine(SearchEngine engine) {
this.engine = engine;
return this;
}
public FluxJdtSearch scope(IJavaSearchScope scope) {
this.scope = scope;
return this;
}
public FluxJdtSearch scope(IProject project) throws JavaModelException {
return scope(JavaCore.create(project));
}
public FluxJdtSearch scope(IJavaProject project) throws JavaModelException {
return scope(searchScope(project));
}
public FluxJdtSearch bufferSize(int bufferSize) {
this.bufferSize = bufferSize;
return this;
}
public FluxJdtSearch pattern(SearchPattern pattern) {
this.pattern = pattern;
return this;
}
/**
* Create a search scope that includes a given project and its dependencies.
*/
public static IJavaSearchScope searchScope(IJavaProject javaProject, boolean includeBinaries) throws JavaModelException {
int includeMask =
IJavaSearchScope.REFERENCED_PROJECTS |
IJavaSearchScope.SOURCES;
if (includeBinaries) {
includeMask = includeMask | IJavaSearchScope.APPLICATION_LIBRARIES;
}
return SearchEngine.createJavaSearchScope(new IJavaElement[] {javaProject}, includeMask);
}
public static IJavaSearchScope searchScope(IJavaProject javaProject) throws JavaModelException {
return searchScope(javaProject, true);
}
/**
* Implementation of {@link SearchRequestor} that emits search results to an {@link ReplayProcessor}
* with replay capability.
*
* @author Kris De Volder
*/
class FluxSearchRequestor extends SearchRequestor {
private boolean isCanceled = false;
private ReplayProcessor<SearchMatch> emitter = ReplayProcessor.<SearchMatch>create(bufferSize).connect();
private Flux<SearchMatch> flux = emitter.doOnCancel(() -> isCanceled=true);
public Flux<SearchMatch> asFlux() {
return flux;
}
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
if (isCanceled) {
debug("!!!! canceling search !!!!");
//Stop searching
throw new OperationCanceledException();
}
emitter.onNext(match);
}
public void cancel() {
isCanceled = true;
}
public void done() {
emitter.onComplete();
}
}
protected SearchEngine searchEngine() {
return new SearchEngine();
}
protected SearchParticipant[] participants() {
return new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()};
}
public Flux<SearchMatch> search() {
validate();
if (scope==null) {
return Flux.empty();
}
final FluxSearchRequestor requestor = new FluxSearchRequestor();
Job job = new Job("Search for "+pattern) {
@Override
protected IStatus run(IProgressMonitor monitor) {
long start = System.currentTimeMillis();
debug("Starting search for '"+pattern+"'");
try {
searchEngine().search(pattern, participants, scope, requestor, monitor);
requestor.done();
} catch (Exception e) {
debug("Canceled search for: "+pattern);
debug(" exception: "+ExceptionUtil.getMessage(e));
long duration = System.currentTimeMillis() - start;
debug(" duration: "+duration+" ms");
requestor.cancel();
}
return Status.OK_STATUS;
}
};
job.setSystem(useSystemJob);
job.setPriority(jobPriority);
job.schedule();
return requestor.asFlux();
}
private void validate() {
Assert.isNotNull(engine, "engine");
//We allow scope to be set to null. This means there's no valid scope (or empty scope)
// and the search should just return no results.
// Assert.isNotNull(scope, "scope");
Assert.isNotNull(pattern, "pattern");
Assert.isNotNull(participants, "participants");
Assert.isLegal(bufferSize > 0);
}
public static IJavaSearchScope workspaceScope(boolean includeBinaries) {
if (includeBinaries) {
return SearchEngine.createWorkspaceScope();
} else {
List<IJavaProject> projects = new ArrayList<>();
for (IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
try {
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
IJavaProject jp = JavaCore.create(p);
projects.add(jp);
}
} catch (Exception e) {
FrameworkCoreActivator.log(e);
}
}
int includeMask = IJavaSearchScope.SOURCES;
return SearchEngine.createJavaSearchScope(projects.toArray(new IJavaElement[projects.size()]), includeMask);
}
}
}