Handle project create and delete in jdt.ls classpath service

This commit is contained in:
Kris De Volder
2018-05-04 14:19:20 -07:00
parent 5e850fb9fd
commit 6a5a023557
3 changed files with 226 additions and 117 deletions

View File

@@ -8,8 +8,9 @@
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.tooling.jdt.ls.commons;
package org.springframework.tooling.jdt.ls.commons.test;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
@@ -18,17 +19,27 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.JavaCore;
import org.junit.After;
import org.junit.Test;
import org.springframework.tooling.jdt.ls.commons.Logger;
import org.springframework.tooling.jdt.ls.commons.classpath.Classpath;
import org.springframework.tooling.jdt.ls.commons.classpath.Classpath.CPE;
import org.springframework.tooling.jdt.ls.commons.classpath.ClientCommandExecutor;
@@ -38,7 +49,46 @@ import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition;
import junit.framework.AssertionFailedError;
public class ClasspathListenerHandlerTest {
private MockClasspathCache classpaths = new MockClasspathCache();
private ReusableClasspathListenerHandler service = new ReusableClasspathListenerHandler(classpaths);
@Test public void classpathIsSentForExistingProject() throws Exception {
String projectName = "classpath-test-simple-java-project";
IProject project = createTestProject(projectName);
File loc = project.getLocation().toFile();
service.addClasspathListener(classpaths.commandId);
ACondition.waitFor("Project with classpath to appear", Duration.ofSeconds(50), () -> {
Classpath cp = classpaths.getFor(loc).classpath;
assertTrue(cp.getEntries().stream().filter(cpe -> Classpath.isSource(cpe)).count()==1); //has 1 source entry
assertClasspath(cp, cp.getEntries().stream().filter(cpe -> Classpath.isBinary(cpe) && cpe.isSystem()).count()>=1); //has some system libraries
});
}
@Test public void classpathIsSentForNewProject_and_removedForDeletedProject() throws Exception {
service.addClasspathListener(classpaths.commandId);
String projectName = "classpath-test-simple-java-project";
IProject project = createTestProject(projectName);
File loc = project.getLocation().toFile();
ACondition.waitFor("Project with classpath to appear", Duration.ofSeconds(5), () -> {
Classpath cp = classpaths.getFor(loc).classpath;
assertTrue(cp.getEntries().stream().filter(cpe -> Classpath.isSource(cpe)).count()==1); //has 1 source entry
assertClasspath(cp, cp.getEntries().stream().filter(cpe -> Classpath.isBinary(cpe) && cpe.isSystem()).count()>=1); //has some system libraries
});
Logger.log("=== Deleteing project");
project.delete(false, true, null);
ACondition.waitFor("Project classpath to disapear", Duration.ofSeconds(5), () -> {
Info cp = classpaths.getFor(loc);
assertNull(cp);
});
}
///////////// harness stuff below ///////////////////////////////////////////////
static class Info {
public final String name;
public final Classpath classpath;
@@ -52,7 +102,6 @@ public class ClasspathListenerHandlerTest {
public class MockClasspathCache implements ClientCommandExecutor {
String commandId = RandomStringUtils.randomAlphabetic(8);
Map<File, Info> classpaths = new HashMap<>();
@@ -65,47 +114,34 @@ public class ClasspathListenerHandlerTest {
String name = (String) params[1];
boolean deleted = (boolean) params[2];
if (deleted) {
System.out.println("DELETING "+name);
classpaths.remove(projectLoc);
} else {
Classpath cp = (Classpath) params[3];
System.out.println("PUT "+name+" "+cp.getEntries().size()+" entries");
classpaths.put(projectLoc, new Info(name, cp));
}
}
return "whatever";
}
public synchronized Info getFor(IProject project) {
File location = project.getLocation().toFile();
public synchronized Info getFor(File location) {
return classpaths.get(location);
}
public void dispose() {
public void dispose() throws Exception {
service.removeClasspathListener(commandId);
deleteAllProjects();
}
}
private MockClasspathCache classpaths = new MockClasspathCache();
private ReusableClasspathListenerHandler service = new ReusableClasspathListenerHandler(classpaths);
@After
public void tearDown() {
public void tearDown() throws Exception {
classpaths.dispose();
assertTrue(service.hasNoActiveSubscriptions());
}
@Test
public void classpathIsSentForExistingProject() throws Exception {
String projectName = "classpath-test-simple-java-project";
IProject project = createTestProject(projectName);
service.addClasspathListener(classpaths.commandId);
ACondition.waitFor("Project with classpath to appear", Duration.ofSeconds(5), () -> {
Classpath cp = classpaths.getFor(project).classpath;
assertTrue(cp.getEntries().stream().filter(cpe -> Classpath.isSource(cpe)).count()==1); //has 1 source entry
assertClasspath(cp, cp.getEntries().stream().filter(cpe -> Classpath.isBinary(cpe) && cpe.isSystem()).count()>=1); //has some system libraries
});
}
private static void assertClasspath(Classpath cp, boolean b) {
if (!b) {
StringBuilder buf = new StringBuilder();
@@ -130,5 +166,28 @@ public class ClasspathListenerHandlerTest {
return project;
}
public static void deleteAllProjects() throws Exception {
CompletableFuture<Void> done = new CompletableFuture<Void>();
WorkspaceJob job = new WorkspaceJob("Delete projects") {
@Override public IStatus runInWorkspace(IProgressMonitor arg0) throws CoreException {
try {
ResourcesPlugin.getWorkspace().getRuleFactory().buildRule();
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
for (IProject project : allProjects) {
project.refreshLocal(IResource.DEPTH_INFINITE, null);
project.close(null);
project.delete(false, true, new NullProgressMonitor());
}
done.complete(null);
} catch (Throwable e) {
done.completeExceptionally(e);
}
return Status.OK_STATUS;
}
};
job.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
job.schedule();
done.get();
}
}

View File

@@ -32,86 +32,96 @@ import static org.springframework.tooling.jdt.ls.commons.Logger.*;
*/
public class ClasspathListenerManager {
public interface ClasspathListener {
public abstract void classpathChanged(IJavaProject jp);
}
public interface ClasspathListener {
public abstract void classpathChanged(IJavaProject jp);
}
private class MyListener implements IElementChangedListener {
private class MyListener implements IElementChangedListener {
@Override
public void elementChanged(ElementChangedEvent event) {
visit(event.getDelta());
}
@Override
public void elementChanged(ElementChangedEvent event) {
Logger.log("changeEvent = "+event);
visit(event.getDelta());
}
private void visit(IJavaElementDelta delta) {
IJavaElement el = delta.getElement();
switch (el.getElementType()) {
case IJavaElement.JAVA_MODEL:
visitChildren(delta);
break;
case IJavaElement.JAVA_PROJECT:
if (isClasspathChanged(delta.getFlags())) {
listener.classpathChanged((IJavaProject)el);
}
break;
default:
break;
}
}
private void visit(IJavaElementDelta delta) {
IJavaElement el = delta.getElement();
switch (el.getElementType()) {
case IJavaElement.JAVA_MODEL:
visitChildren(delta);
break;
case IJavaElement.JAVA_PROJECT:
if (isCreatedOrDeleted(delta) || isClasspathChanged(delta.getFlags())) {
listener.classpathChanged((IJavaProject)el);
}
break;
default:
break;
}
}
private boolean isClasspathChanged(int flags) {
return 0!= (flags & (
IJavaElementDelta.F_CLASSPATH_CHANGED |
IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED
));
}
private boolean isCreatedOrDeleted(IJavaElementDelta delta) {
int kind = delta.getKind();
return kind == IJavaElementDelta.ADDED || kind==IJavaElementDelta.REMOVED;
}
public void visitChildren(IJavaElementDelta delta) {
for (IJavaElementDelta c : delta.getAffectedChildren()) {
visit(c);
}
}
}
private boolean isClasspathChanged(int flags) {
return 0!= (flags & (
IJavaElementDelta.F_CLASSPATH_CHANGED |
IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED |
IJavaElementDelta.F_CLOSED |
IJavaElementDelta.F_OPENED
));
}
private ClasspathListener listener;
private MyListener myListener;
public void visitChildren(IJavaElementDelta delta) {
for (IJavaElementDelta c : delta.getAffectedChildren()) {
visit(c);
}
}
}
/**
* @param initialEvent If true, events are fired immediately on all existing java
* projects, treating the connection of the listener itself as a change event.
* This allows clients to become aware of all classpaths from the start and
* continually monitor them for changes from that point onward.
*/
public ClasspathListenerManager(ClasspathListener listener, boolean initialEvent) {
log("Setting up ClasspathListenerManager");
this.listener = listener;
if (initialEvent) {
log("Sending initial event for all projects ...");
for (IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
log("project = "+p);
try {
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
IJavaProject jp = JavaCore.create(p);
listener.classpathChanged(jp);
}
} catch (CoreException e) {
Logger.log(e);
}
}
log("Sending initial event for all projects DONE");
}
JavaCore.addElementChangedListener(myListener=new MyListener(), ElementChangedEvent.POST_CHANGE);
}
private ClasspathListener listener;
private MyListener myListener;
public ClasspathListenerManager(ClasspathListener listener) {
this(listener, false);
}
/**
* @param initialEvent If true, events are fired immediately on all existing java
* projects, treating the connection of the listener itself as a change event.
* This allows clients to become aware of all classpaths from the start and
* continually monitor them for changes from that point onward.
*/
public ClasspathListenerManager(ClasspathListener listener, boolean initialEvent) {
log("Setting up ClasspathListenerManager");
this.listener = listener;
JavaCore.addElementChangedListener(myListener=new MyListener(), ElementChangedEvent.POST_CHANGE);
if (initialEvent) {
log("Sending initial event for all projects ...");
for (IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
log("project "+p.getName() +" ..." );
try {
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
IJavaProject jp = JavaCore.create(p);
listener.classpathChanged(jp);
} else {
log("project "+p.getName() +" SKIPPED" );
}
} catch (CoreException e) {
Logger.log(e);
}
}
log("Sending initial event for all projects DONE");
}
}
public void dispose() {
if (myListener!=null) {
JavaCore.removeElementChangedListener(myListener);
myListener = null;
}
}
public ClasspathListenerManager(ClasspathListener listener) {
this(listener, false);
}
public void dispose() {
if (myListener!=null) {
JavaCore.removeElementChangedListener(myListener);
myListener = null;
}
}
}

View File

@@ -12,6 +12,8 @@ package org.springframework.tooling.jdt.ls.commons.classpath;
import static org.springframework.tooling.jdt.ls.commons.Logger.log;
import java.io.File;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
@@ -34,6 +36,24 @@ public class ReusableClasspathListenerHandler {
this.conn = conn;
log("Instantiating ReusableClasspathListenerHandler");
}
/**
* To keep track of project locations. Without this we can't properly handle deleting events because
* deleted projects (no longer) have a location. So we can only send a proper 'project with this location'
* was deleted' events if we keep track of project locations ourselves.
*/
private Map<String, URI> projectLocations = new HashMap<>();
private URI getProjectLocation(IJavaProject jp) {
URI loc = jp.getProject().getLocationURI();
if (loc!=null) {
return loc;
} else {
//fallback on what we stored ourselves.
return projectLocations.get(jp.getElementName());
}
}
class Subscribptions {
@@ -56,35 +76,55 @@ public class ReusableClasspathListenerHandler {
private void sendNotification(String callbackCommandId, IJavaProject jp) {
//TODO: make one Job to accumulate all requested notification and work more efficiently by batching
// and avoiding multiple executions of duplicated requests.
new Job("SendClasspath notification") {
Job job = new Job("SendClasspath notification") {
@Override
protected IStatus run(IProgressMonitor monitor) {
log("Classpath changed " + jp.getElementName());
String project = jp.getProject().getLocationURI().toString();
boolean deleted = !jp.exists();
// JavaClientConnection conn = JavaLanguageServerPlugin.getInstance().getClientConnection();
String projectName = jp.getElementName();
Classpath classpath = null;
if (!deleted) {
synchronized (projectLocations) { //Could use some Eclipse job rule. But its really a bit of a PITA to create the right one.
try {
classpath = ClasspathUtil.resolve(jp);
log("Preparing classpath changed notification " + jp.getElementName());
URI projectLoc = getProjectLocation(jp);
if (projectLoc==null) {
Logger.log("Could not send event for project because no project location: "+jp.getElementName());
} else {
boolean exsits = jp.exists();
boolean open = true; // WARNING: calling is jp.isOpen is unreliable and subject to race condition. After a POST_CHAGE project open event
// this should be true but it typically is not unless you wait for some time. No idea how you would know
// how long you should wait (200ms is not enough, and that seems pretty long). Isn't it kind of the point
// for a 'POST_CHANGE' event to come **after** model has already changed? I guess not in Eclipse.
// So we will just pretend / assume project is always open. If resolving classpath fails because it is not
// open... so be it (there will be no classpath... this is expected for closed project, so that is fine).
boolean deleted = !(exsits && open);
Logger.log("exists = "+exsits +" open = "+open +" => deleted = "+deleted);
String projectName = jp.getElementName();
Classpath classpath = null;
if (deleted) {
projectLocations.remove(projectName);
} else {
projectLocations.put(projectName, projectLoc);
try {
classpath = ClasspathUtil.resolve(jp);
} catch (Exception e) {
Logger.log(e);
}
}
try {
Logger.log("executing callback "+callbackCommandId+" "+projectName+" "+deleted+" "+(classpath==null ? "" : classpath.getEntries().size()));
conn.executeClientCommand(callbackCommandId, projectLoc.toString(), projectName, deleted, classpath);
Logger.log("executing callback "+callbackCommandId+" SUCCESS");
} catch (Exception e) {
Logger.log("executing callback "+callbackCommandId+" FAILED");
Logger.log(e);
}
}
} catch (Exception e) {
Logger.log(e);
}
return Status.OK_STATUS;
}
try {
Logger.log("executing callback "+callbackCommandId+" "+projectName+" "+deleted+" "+(classpath==null ? "" : classpath.getEntries().size()));
conn.executeClientCommand(callbackCommandId, project, projectName, deleted, classpath);
Logger.log("executing callback "+callbackCommandId+" SUCCESS");
} catch (Exception e) {
Logger.log("executing callback "+callbackCommandId+" FAILED");
Logger.log(e);
}
return Status.OK_STATUS;
}
}
.schedule();
};
job.schedule();
}
public synchronized void unsubscribe(String callbackCommandId) {
@@ -109,9 +149,9 @@ public class ReusableClasspathListenerHandler {
private Subscribptions subscribptions = new Subscribptions();
public Object removeClasspathListener(String callbackCommandId) {
log("ClasspathListenerHandler addClasspathListener " + callbackCommandId);
log("ClasspathListenerHandler removeClasspathListener " + callbackCommandId);
subscribptions.unsubscribe(callbackCommandId);
log("ClasspathListenerHandler addClasspathListener " + callbackCommandId + " => OK");
log("ClasspathListenerHandler removeClasspathListener " + callbackCommandId + " => OK");
return "ok";
}