From cee1005bb8f460042781c4dc30bd9c70014012c5 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Nov 2017 12:06:26 -0500 Subject: [PATCH] PT #151547534 Cache classpath in a file --- .../java/BootJavaLanguageServerParams.java | 30 ++- .../handlers/BootJavaCompletionEngine.java | 2 +- .../java/handlers/BootJavaHoverProvider.java | 2 +- .../handlers/BootJavaReferencesHandler.java | 2 +- .../boot/java/utils/BootProjectUtil.java | 2 +- .../boot/java/utils/CompilationUnitCache.java | 2 +- .../vscode/boot/java/utils/SpringIndexer.java | 2 +- .../harness/BootLanguageServerHarness.java | 4 +- .../boot/BootPropertiesLanguageServer.java | 6 +- .../boot/metadata/ResourceHintProvider.java | 2 +- .../commons/gradle/GradleJavaProject.java | 30 +-- .../commons/gradle/GradleProjectCache.java | 17 +- .../gradle/GradleProjectClasspath.java | 112 +++++--- .../commons/gradle/GradleProjectTest.java | 100 ++++--- .../empty-gradle-project/build.newgradle | 34 +++ .../commons/commons-java/pom.xml | 5 + .../commons/jandex/JandexClasspath.java | 2 +- .../commons/java/AbstractJavaProject.java | 29 ++ .../java/DelegatingCachedClasspath.java | 247 ++++++++++++++++++ .../ide/vscode/commons/java/IClasspath.java | 9 +- .../ide/vscode/commons/java/IJavaProject.java | 2 + .../java/AbstractFileToProjectCache.java | 39 ++- .../commons/maven/java/MavenJavaProject.java | 34 ++- .../commons/maven/java/MavenProjectCache.java | 16 +- .../maven/java/MavenProjectClasspath.java | 36 ++- .../java/classpathfile/FileClasspath.java | 13 +- .../JavaProjectWithClasspathFile.java | 15 +- .../JavaProjectWithClasspathFileCache.java | 7 +- .../maven/MavenProjectManagerTest.java | 89 +++++-- .../pom.newxml | 72 +++++ 30 files changed, 786 insertions(+), 176 deletions(-) create mode 100644 headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle create mode 100644 headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java create mode 100644 headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java create mode 100644 headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java index ca7a8afba..721f5add3 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java; +import java.nio.file.Paths; import java.time.Duration; import java.util.Arrays; @@ -21,6 +22,7 @@ import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.commons.gradle.GradleCore; import org.springframework.ide.vscode.commons.gradle.GradleProjectCache; import org.springframework.ide.vscode.commons.gradle.GradleProjectFinder; +import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.CompositeJavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.CompositeProjectOvserver; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -60,10 +62,10 @@ public class BootJavaLanguageServerParams { // Initialize project finders, project caches and project observers FileObserver fileObserver = server.getWorkspaceService().getFileObserver(); CompositeJavaProjectFinder javaProjectFinder = new CompositeJavaProjectFinder(); - MavenProjectCache mavenProjectCache = new MavenProjectCache(fileObserver, MavenCore.getDefault()); + MavenProjectCache mavenProjectCache = new MavenProjectCache(fileObserver, MavenCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new MavenProjectFinder(mavenProjectCache)); - GradleProjectCache gradleProjectCache = new GradleProjectCache(fileObserver, GradleCore.getDefault()); + GradleProjectCache gradleProjectCache = new GradleProjectCache(fileObserver, GradleCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new GradleProjectFinder(gradleProjectCache)); CompositeProjectOvserver projectObserver = new CompositeProjectOvserver(Arrays.asList(mavenProjectCache, gradleProjectCache)); @@ -78,5 +80,29 @@ public class BootJavaLanguageServerParams { }; } + public static LSFactory createTestDefault() { + return (SimpleLanguageServer server) -> { + // Initialize project finders, project caches and project observers + FileObserver fileObserver = server.getWorkspaceService().getFileObserver(); + CompositeJavaProjectFinder javaProjectFinder = new CompositeJavaProjectFinder(); + MavenProjectCache mavenProjectCache = new MavenProjectCache(fileObserver, MavenCore.getDefault(), false, null); + mavenProjectCache.setAlwaysFireEventOnFileChanged(true); + javaProjectFinder.addJavaProjectFinder(new MavenProjectFinder(mavenProjectCache)); + + GradleProjectCache gradleProjectCache = new GradleProjectCache(fileObserver, GradleCore.getDefault(), false, null); + gradleProjectCache.setAlwaysFireEventOnFileChanged(true); + javaProjectFinder.addJavaProjectFinder(new GradleProjectFinder(gradleProjectCache)); + + CompositeProjectOvserver projectObserver = new CompositeProjectOvserver(Arrays.asList(mavenProjectCache, gradleProjectCache)); + + return new BootJavaLanguageServerParams( + javaProjectFinder.filter(BootProjectUtil::isBootProject), + projectObserver, + new DefaultSpringPropertyIndexProvider(javaProjectFinder, projectObserver), + RunningAppProvider.DEFAULT, + SpringLiveHoverWatchdog.DEFAULT_INTERVAL + ); + }; + } } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java index 214e65303..e93946ab8 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java @@ -111,7 +111,7 @@ public class BootJavaCompletionEngine implements ICompletionEngine { private String[] getClasspathEntries(IDocument doc) throws Exception { IJavaProject project = this.projectFinder.find(new TextDocumentIdentifier(doc.getUri())).get(); IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index 51099c297..ca5dcd0a5 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -213,7 +213,7 @@ public class BootJavaHoverProvider implements HoverHandler { try { IClasspath classpath = project.getClasspath(); if (classpath!=null) { - return classpath.getClasspathEntries().anyMatch(cpe -> { + return classpath.getClasspathEntries().stream().anyMatch(cpe -> { String name = cpe.getFileName().toString(); return name.startsWith("spring-boot-actuator-"); }); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java index a325aee1f..2e032aa7e 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java @@ -126,7 +126,7 @@ public class BootJavaReferencesHandler implements ReferencesHandler { private String[] getClasspathEntries(IDocument doc) throws Exception { IJavaProject project = this.projectFinder.find(new TextDocumentIdentifier(doc.getUri())).get(); IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java index 9994c7da6..dd3272316 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java @@ -22,7 +22,7 @@ public class BootProjectUtil { try { IClasspath cp = jp.getClasspath(); if (cp!=null) { - return cp.getClasspathEntries().anyMatch(cpe -> isBootEntry(cpe)); + return cp.getClasspathEntries().stream().anyMatch(cpe -> isBootEntry(cpe)); } } catch (Exception e) { Log.log(e); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index 4be7e4429..776c638d1 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -133,7 +133,7 @@ public final class CompilationUnitCache { return new String[0]; } else { IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java index 2c423a083..811d93b2c 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java @@ -456,7 +456,7 @@ public class SpringIndexer { private String[] getClasspathEntries(IJavaProject project) throws Exception { IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java index 6c68d7772..997df25de 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java @@ -43,7 +43,7 @@ public class BootLanguageServerHarness extends LanguageServerHarness defaultsFactory = BootJavaLanguageServerParams.createDefault(); + LSFactory defaultsFactory = BootJavaLanguageServerParams.createTestDefault(); private JavaProjectFinder projectFinder = null; private ProjectObserver projectObserver = null; private SpringPropertyIndexProvider indexProvider = null; @@ -92,7 +92,7 @@ public class BootLanguageServerHarness extends LanguageServerHarness { LSFactory params = (server) -> { - BootJavaLanguageServerParams defaults = BootJavaLanguageServerParams.createDefault().create(server); + BootJavaLanguageServerParams defaults = BootJavaLanguageServerParams.createTestDefault().create(server); return new BootJavaLanguageServerParams( builder.projectFinder==null?defaults.projectFinder:builder.projectFinder, builder.projectObserver==null?defaults.projectObserver:builder.projectObserver, diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java index 422915941..e0983655d 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot; +import java.nio.file.Paths; import java.util.Arrays; import org.springframework.ide.vscode.boot.common.PropertyCompletionFactory; @@ -25,6 +26,7 @@ import org.springframework.ide.vscode.boot.yaml.reconcile.ApplicationYamlReconci import org.springframework.ide.vscode.commons.gradle.GradleCore; import org.springframework.ide.vscode.commons.gradle.GradleProjectCache; import org.springframework.ide.vscode.commons.gradle.GradleProjectFinder; +import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider; @@ -120,10 +122,10 @@ public class BootPropertiesLanguageServer extends SimpleLanguageServer { documents.onHover(hoverEngine::getHover); // Initialize project finders, project caches and project observers - MavenProjectCache mavenProjectCache = new MavenProjectCache(getWorkspaceService().getFileObserver(), MavenCore.getDefault()); + MavenProjectCache mavenProjectCache = new MavenProjectCache(getWorkspaceService().getFileObserver(), MavenCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new MavenProjectFinder(mavenProjectCache)); - GradleProjectCache gradleProjectCache = new GradleProjectCache(getWorkspaceService().getFileObserver(), GradleCore.getDefault()); + GradleProjectCache gradleProjectCache = new GradleProjectCache(getWorkspaceService().getFileObserver(), GradleCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new GradleProjectFinder(gradleProjectCache)); projectObserver = new CompositeProjectOvserver(Arrays.asList(mavenProjectCache, gradleProjectCache)); diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java index beb3891ee..bb630ce8f 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java @@ -63,7 +63,7 @@ public class ResourceHintProvider implements ValueProviderStrategy { private static class ClasspathHints extends CachingValueProvider { @Override protected Flux getValuesAsync(IJavaProject javaProject, String query) { - return Flux.fromStream(javaProject.getClasspath().getClasspathResources().distinct().map(StsValueHint::create)); + return Flux.fromStream(javaProject.getClasspath().getClasspathResources().stream().distinct().map(StsValueHint::create)); } } diff --git a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java index 03596a937..474529bc0 100644 --- a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java +++ b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java @@ -11,9 +11,10 @@ package org.springframework.ide.vscode.commons.gradle; import java.io.File; +import java.nio.file.Path; -import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.java.AbstractJavaProject; +import org.springframework.ide.vscode.commons.java.DelegatingCachedClasspath; /** * Implementation of Gradle Java project @@ -21,16 +22,18 @@ import org.springframework.ide.vscode.commons.util.Log; * @author Alex Boyko * */ -public class GradleJavaProject implements IJavaProject { +public class GradleJavaProject extends AbstractJavaProject { - private GradleCore gradle; - private GradleProjectClasspath classpath; + private DelegatingCachedClasspath classpath; private File projectDir; - public GradleJavaProject(GradleCore gradle, File projectDir) throws GradleException { - this.gradle = gradle; + public GradleJavaProject(GradleCore gradle, File projectDir, Path projectDataCache) { + super(projectDataCache); this.projectDir = projectDir; - this.classpath = new GradleProjectClasspath(gradle, projectDir); + this.classpath = new DelegatingCachedClasspath( + () -> new GradleProjectClasspath(gradle, projectDir), + projectDataCache == null ? null : projectDataCache.resolve(DelegatingCachedClasspath.CLASSPATH_DATA_CACHE_FILE).toFile() + ); } public File getLocation() { @@ -38,16 +41,13 @@ public class GradleJavaProject implements IJavaProject { } @Override - public GradleProjectClasspath getClasspath() { + public DelegatingCachedClasspath getClasspath() { return classpath; } - void update() { - try { - this.classpath = new GradleProjectClasspath(gradle, projectDir); - } catch (GradleException e) { - Log.log(e); - } + boolean update() { + return classpath.update(); } + } diff --git a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java index da2185273..94c9fbbb8 100644 --- a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java +++ b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.gradle; import java.io.File; +import java.nio.file.Path; import org.springframework.ide.vscode.commons.languageserver.java.AbstractFileToProjectCache; import org.springframework.ide.vscode.commons.util.FileObserver; @@ -25,19 +26,25 @@ public class GradleProjectCache extends AbstractFileToProjectCache gradleProject; private Supplier buildEnvironment; - public GradleProjectClasspath(GradleCore gradle, File projectDir) throws GradleException { + public GradleProjectClasspath(GradleCore gradle, File projectDir) { super(); - this.gradleProject = gradle.getModel(projectDir, EclipseProject.class); + this.gradleProject = Suppliers.memoize(() -> { + try { + return gradle.getModel(projectDir, EclipseProject.class); + } catch (GradleException e) { + Log.log(e); + return null; + } + }); this.buildEnvironment = Suppliers.memoize(() -> { try { return gradle.getModel(projectDir, BuildEnvironment.class); @@ -84,7 +92,10 @@ public class GradleProjectClasspath extends JandexClasspath { } public EclipseProject getRootProject() { - EclipseProject root = this.gradleProject; + EclipseProject root = this.gradleProject.get(); + if (root == null) { + return root; + } while(root.getParent() != null) { root = root.getParent(); } @@ -92,15 +103,21 @@ public class GradleProjectClasspath extends JandexClasspath { } @Override - public Stream getClasspathEntries() throws Exception { + public ImmutableList getClasspathEntries() throws Exception { EclipseProject root = getRootProject(); - return Stream.concat(gradleProject.getClasspath().stream().map(dep -> dep.getFile().toPath()), - gradleProject.getProjectDependencies().stream() - .map(d -> findPeer(root, d.getTargetProject().getName())) - .filter(o -> o.isPresent()) - .map(o -> o.get()) - .map(p -> p.getProjectDirectory().toPath().resolve(p.getOutputLocation().getPath())) - ); + EclipseProject project = gradleProject.get(); + if (project == null) { + return ImmutableList.of(); + } else { + ImmutableList classpathEntries = ImmutableList.copyOf(Stream.concat(project.getClasspath().stream().map(dep -> dep.getFile().toPath()), + project.getProjectDependencies().stream() + .map(d -> findPeer(root, d.getTargetProject().getName())) + .filter(o -> o.isPresent()) + .map(o -> o.get()) + .map(p -> p.getProjectDirectory().toPath().resolve(p.getOutputLocation().getPath())) + ).collect(Collectors.toList())); + return classpathEntries; + } } private Optional findPeer(EclipseProject root, String name) { @@ -108,26 +125,33 @@ public class GradleProjectClasspath extends JandexClasspath { } @Override - public Stream getClasspathResources() { - return gradleProject.getSourceDirectories().stream().map(sourceDirectory -> sourceDirectory.getDirectory()).flatMap(folder -> { - try { - return Files.walk(folder.toPath()) - .filter(path -> Files.isRegularFile(path)) - .map(path -> folder.toPath().relativize(path)) - .map(relativePath -> relativePath.toString()) - .filter(pathString -> !pathString.endsWith(".java") && !pathString.endsWith(".class")); - } catch (IOException e) { - return Stream.empty(); - } - }); + public ImmutableList getClasspathResources() { + EclipseProject project = gradleProject.get(); + if (project == null) { + return ImmutableList.of(); + } else { + return ImmutableList.copyOf(project.getSourceDirectories().stream().map(sourceDirectory -> sourceDirectory.getDirectory()).flatMap(folder -> { + try { + return Files.walk(folder.toPath()) + .filter(path -> Files.isRegularFile(path)) + .map(path -> folder.toPath().relativize(path)) + .map(relativePath -> relativePath.toString()) + .filter(pathString -> !pathString.endsWith(".java") && !pathString.endsWith(".class")); + } catch (IOException e) { + return Stream.empty(); + } + }).toArray(String[]::new)); + } } public Path getOutputFolder() { - return gradleProject.getProjectDirectory().toPath().resolve(gradleProject.getOutputLocation().getPath()); + EclipseProject project = gradleProject.get(); + return project == null ? null : project.getProjectDirectory().toPath().resolve(project.getOutputLocation().getPath()); } public String getName() { - return gradleProject.getName(); + EclipseProject project = gradleProject.get(); + return project == null ? null : project.getName(); } public boolean exists() { @@ -136,20 +160,23 @@ public class GradleProjectClasspath extends JandexClasspath { @Override protected IJavadocProvider createParserJavadocProvider(File classpathResource) { - if (classpathResource.isDirectory()) { - Optional classpathFolder = gradleProject.getSourceDirectories().stream() - .map(dir -> dir.getDirectory()) - .filter(dir -> classpathResource.toPath().startsWith(dir.toPath())) - .findFirst(); - if (classpathFolder.isPresent()) { - return new ParserJavadocProvider(type -> { - return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER - .sourceUrl(classpathFolder.get().toURI().toURL(), type); - }); + EclipseProject project = gradleProject.get(); + if (project != null) { + if (classpathResource.isDirectory()) { + Optional classpathFolder = project.getSourceDirectories().stream() + .map(dir -> dir.getDirectory()) + .filter(dir -> classpathResource.toPath().startsWith(dir.toPath())) + .findFirst(); + if (classpathFolder.isPresent()) { + return new ParserJavadocProvider(type -> { + return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER + .sourceUrl(classpathFolder.get().toURI().toURL(), type); + }); + } + } else { + } - } else { - } return null; } @@ -222,4 +249,13 @@ public class GradleProjectClasspath extends JandexClasspath { return new File(JandexIndex.getIndexFolder().toString(), jarFile.getName() + "-" + suffix + ".jdx"); } + @Override + public boolean equals(Object obj) { + if (obj instanceof GradleProjectClasspath) { + return super.equals(obj); + } + return false; + } + + } diff --git a/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java b/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java index 2068c29a1..4e207ac69 100644 --- a/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java +++ b/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java @@ -13,21 +13,26 @@ package org.springframework.ide.vscode.commons.gradle; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; +import org.assertj.core.util.Files; import org.junit.Test; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener; import org.springframework.ide.vscode.commons.util.BasicFileObserver; +import com.google.common.collect.ImmutableList; + /** * Tests covering Gradle project data * @@ -36,15 +41,27 @@ import org.springframework.ide.vscode.commons.util.BasicFileObserver; */ public class GradleProjectTest { + private static void writeContent(File file, String content) throws IOException { + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.write(content); + } finally { + writer.close(); + } + } + private GradleJavaProject getGradleProject(String projectName) throws Exception { Path testProjectPath = Paths.get(GradleProjectTest.class.getResource("/" + projectName).toURI()); - return new GradleJavaProject(GradleCore.getDefault(), testProjectPath.toFile()); + GradleJavaProject gradleJavaProject = new GradleJavaProject(GradleCore.getDefault(), testProjectPath.toFile(), null); + gradleJavaProject.update(); + return gradleJavaProject; } @Test public void testEclipseGradleProject() throws Exception { GradleJavaProject project = getGradleProject("empty-gradle-project"); - Set calculatedClassPath = project.getClasspath().getClasspathEntries().collect(Collectors.toSet()); + ImmutableList calculatedClassPath = project.getClasspath().getClasspathEntries(); assertEquals(48, calculatedClassPath.size()); } @@ -57,7 +74,7 @@ public class GradleProjectTest { @Test public void gradleClasspathResource() throws Exception { GradleJavaProject project = getGradleProject("test-app-1"); - List resources = project.getClasspath().getClasspathResources().collect(Collectors.toList()); + List resources = project.getClasspath().getClasspathResources(); assertArrayEquals(new String[] {"test-resource-1.txt"}, resources.toArray(new String[resources.size()])); } @@ -65,38 +82,57 @@ public class GradleProjectTest { public void testGradleFileChanges() throws Exception { Path testProjectPath = Paths.get(GradleProjectTest.class.getResource("/empty-gradle-project").toURI()); File gradleFile = testProjectPath.resolve(GradleCore.GRADLE_BUILD_FILE).toFile(); - BasicFileObserver fileObserver = new BasicFileObserver(); - GradleProjectCache manager = new GradleProjectCache(fileObserver, GradleCore.getDefault()); - IJavaProject[] projectChanged = new IJavaProject[] { null }; - IJavaProject[] projectDeleted = new IJavaProject[] { null }; - manager.addListener(new Listener() { - @Override - public void created(IJavaProject project) {} - - @Override - public void changed(IJavaProject project) { - projectChanged[0] = project; - } - @Override - public void deleted(IJavaProject project) { - projectDeleted[0] = project; - } - }); - // Get the project from cache - GradleJavaProject cachedProject = manager.project(gradleFile); - assertNotNull(cachedProject); + String gradelFileContents = Files.contentOf(gradleFile, Charset.defaultCharset()); - fileObserver.notifyFileChanged(gradleFile.toURI().toString()); - assertEquals(cachedProject, projectChanged[0]); - - fileObserver.notifyFileDeleted(gradleFile.toURI().toString()); - assertEquals(cachedProject, projectDeleted[0]); + try { + BasicFileObserver fileObserver = new BasicFileObserver(); + GradleProjectCache manager = new GradleProjectCache(fileObserver, GradleCore.getDefault(), false, null); + IJavaProject[] projectChanged = new IJavaProject[] { null }; + IJavaProject[] projectDeleted = new IJavaProject[] { null }; + + // Get the project from cache + GradleJavaProject cachedProject = manager.project(gradleFile); + assertNotNull(cachedProject); + + manager.addListener(new Listener() { + @Override + public void created(IJavaProject project) {} + + @Override + public void changed(IJavaProject project) { + projectChanged[0] = project; + } + @Override + public void deleted(IJavaProject project) { + projectDeleted[0] = project; + } + }); + + ImmutableList calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(48, calculatedClassPath.size()); + + fileObserver.notifyFileChanged(gradleFile.toURI().toString()); + assertNull(projectChanged[0]); + + writeContent(gradleFile, Files.contentOf(testProjectPath.resolve("build.newgradle").toFile(), Charset.defaultCharset())); + fileObserver.notifyFileChanged(gradleFile.toURI().toString()); + assertNotNull(projectChanged[0]); + assertEquals(cachedProject, projectChanged[0]); + calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(49, calculatedClassPath.size()); + + + fileObserver.notifyFileDeleted(gradleFile.toURI().toString()); + assertEquals(cachedProject, projectDeleted[0]); + } finally { + writeContent(gradleFile, gradelFileContents); + } } @Test public void findGradleProjectWithStandardBuildFile() throws Exception { - GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault())); + GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault(), false, null)); File sourceFile = new File(GradleProjectTest.class.getResource("/test-app-1/src/main/java/Library.java").toURI()); Optional project = finder.find(sourceFile); assertTrue(project.isPresent()); @@ -107,7 +143,7 @@ public class GradleProjectTest { @Test public void findGradleProjectWithNonStandardBuildFile() throws Exception { - GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault())); + GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault(), false, null)); File sourceFile = new File(GradleProjectTest.class.getResource("/test-app-2/src/main/java/Library.java").toURI()); Optional project = finder.find(sourceFile); assertTrue(project.isPresent()); diff --git a/headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle b/headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle new file mode 100644 index 000000000..e01b09e0d --- /dev/null +++ b/headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle @@ -0,0 +1,34 @@ +buildscript { + ext { + springBootVersion = '1.5.1.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' + +jar { + baseName = 'empty-boot-1.4.0-web-app' + version = '0.0.1-SNAPSHOT' +} + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + + +dependencies { + compile('org.springframework.boot:spring-boot-starter-actuator') + compile('org.springframework.boot:spring-boot-starter-web') + compile('org.springframework.boot:spring-boot-devtools') + testCompile('org.springframework.boot:spring-boot-starter-test') +} diff --git a/headless-services/commons/commons-java/pom.xml b/headless-services/commons/commons-java/pom.xml index 756288fb5..52f92d2ea 100644 --- a/headless-services/commons/commons-java/pom.xml +++ b/headless-services/commons/commons-java/pom.xml @@ -46,5 +46,10 @@ reactor-core ${reactor-version} + + org.json + json + 20160810 + \ No newline at end of file diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java index c90066026..63d200b0e 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java @@ -35,7 +35,7 @@ public abstract class JandexClasspath implements IClasspath { protected JandexIndex createIndex() { Stream classpathEntries = Stream.empty(); try { - classpathEntries = getClasspathEntries(); + classpathEntries = getClasspathEntries().stream(); } catch (Exception e) { Log.log(e); } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java new file mode 100644 index 000000000..8bb376491 --- /dev/null +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2017 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.springframework.ide.vscode.commons.java; + +import java.nio.file.Path; + +/** + * Abstract java project. Has a folder to store some project calculated data to speed up access + * + * @author Alex Boyko + * + */ +public abstract class AbstractJavaProject implements IJavaProject { + + final protected Path projectDataCache; + + public AbstractJavaProject(Path projectDataCache) { + this.projectDataCache = projectDataCache; + } + +} diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java new file mode 100644 index 000000000..4e2e4824e --- /dev/null +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) 2017 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.springframework.ide.vscode.commons.java; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.ide.vscode.commons.util.Log; + +import com.google.common.base.Objects; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; + +import reactor.core.publisher.Flux; +import reactor.util.function.Tuple2; + +/** + * A wrapper around the classpath created from a Java project using the data in the project file (maven, gradle) + * The wrapper caches some of classpath data such as + *
  • Classpath entries
  • + *
  • Classpath resources
  • + *
  • Output folder
  • + *
  • Projects' name
  • + * + * The cached classpath data is written to ".sts4-cache/classpath-data.json" and loadedd from it when intance of this classpath is created + * + * Implementation is somewhat experimental at the moment... + * + * @author Alex Boyko + * + * @param a subclass of {@link IClasspath} the delegated to classpath created from current data + */ +public class DelegatingCachedClasspath implements IClasspath { + + public static final String CLASSPATH_DATA_CACHE_FILE = "classpath-data.json"; + + private static final String OUTPUT_FOLDER_PROPERTY = "outputFolder"; + private static final String CLASSPATH_RESOURCES_PROPERTY = "classpathResources"; + private static final String CLASSPATH_ENTRIES_PROPERTY = "classpathEntries"; + private static final String NAME_PROPERTY = "name"; + + protected static class ClasspathData { + + final public String name; + final public Set classpathEntries; + final public Set classpathResources; + final public Path outputFolder; + + public ClasspathData(String name, Set classpathEntries, Set classpathResources, Path outputFolder) { + this.name = name; + this.classpathEntries = classpathEntries; + this.classpathResources = classpathResources; + this.outputFolder = outputFolder; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ClasspathData) { + ClasspathData other = (ClasspathData) obj; + try { + return Objects.equal(name, other.name) + && Objects.equal(classpathEntries, other.classpathEntries) + && Objects.equal(classpathResources, other.classpathResources) + && Objects.equal(outputFolder, outputFolder); + } catch (Throwable t) { + Log.log(t); + } + } + return false; + } + + } + + private AtomicReference cachedData; + private Supplier delegateCreator; + private AtomicReference cachedDelegate; + + final private File cacheFile; + + public DelegatingCachedClasspath(Supplier delegateCreator, File cacheFile) { + super(); + this.cacheFile = cacheFile; + this.cachedDelegate = new AtomicReference<>(delegateCreator.get()); + this.cachedData = new AtomicReference<>(init()); + this.delegateCreator = delegateCreator; + if (!isCached()) { + update(); + } + } + + public T delegate() { + return cachedDelegate.get(); + } + + @Override + public String getName() { + return cachedData.get().name; + } + + @Override + public Path getOutputFolder() { + return cachedData.get().outputFolder; + } + + @Override + public ImmutableList getClasspathEntries() throws Exception { + return ImmutableList.copyOf(cachedData.get().classpathEntries); + } + + @Override + public ImmutableList getClasspathResources() { + return ImmutableList.copyOf(cachedData.get().classpathResources); + } + + public boolean isCached() { + return cacheFile != null && cacheFile.exists(); + } + + private synchronized ClasspathData loadCachedData() { + if (cacheFile != null && cacheFile.exists()) { + try { + JSONObject json = new JSONObject(new JSONTokener(new FileInputStream(cacheFile))); + String name = json.getString(NAME_PROPERTY); + JSONArray classpathEntriesJson = json.optJSONArray(CLASSPATH_ENTRIES_PROPERTY); + JSONArray classpathResourcesJson = json.optJSONArray(CLASSPATH_RESOURCES_PROPERTY); + String outputFolderStr = json.optString(OUTPUT_FOLDER_PROPERTY); + + return new ClasspathData( + name, + classpathEntriesJson == null ? Collections.emptySet() : classpathEntriesJson.toList().stream() + .filter(o -> o instanceof String) + .map(o -> (String) o) + .map(s -> new File(s).toPath()) + .collect(Collectors.toSet()), + classpathResourcesJson == null ? Collections.emptySet() : classpathResourcesJson.toList().stream() + .filter(o -> o instanceof String) + .map(o -> (String) o) + .collect(Collectors.toSet()), + outputFolderStr == null ? null : new File(outputFolderStr).toPath() + ); + } catch (Throwable e) { + Log.log(e); + } + } + return null; + } + + private ClasspathData init() { + ClasspathData data = loadCachedData(); + return data == null ? new ClasspathData(null, Collections.emptySet(), Collections.emptySet(), null) : data; + } + + private synchronized void persistCachedData(ClasspathData data) { + if (cacheFile != null && data != null) { + FileWriter writer = null; + try { + Files.createDirectories(cacheFile.getParentFile().toPath()); + JSONObject json = new JSONObject(); + json.put(NAME_PROPERTY, data.name); + json.put(CLASSPATH_ENTRIES_PROPERTY, data.classpathEntries.stream().map(e -> e.toString()).collect(Collectors.toList())); + json.put(CLASSPATH_RESOURCES_PROPERTY, data.classpathResources); + json.put(OUTPUT_FOLDER_PROPERTY, data.outputFolder); + writer = new FileWriter(cacheFile); + json.write(writer); + } catch (IOException e) { + Log.log(e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + Log.log(e); + } + } + } + } + } + + public boolean update() { + final ClasspathData newData = createClasspathData(); + if (!Objects.equal(cachedData.get(), newData)) { + cachedData.set(newData); + persistCachedData(newData); + return true; + } + return false; + } + + @Override + public boolean exists() { + return cachedDelegate.get().exists(); + } + + @Override + public IType findType(String fqName) { + return cachedDelegate.get().findType(fqName); + } + + @Override + public Flux> fuzzySearchTypes(String searchTerm, Predicate typeFilter) { + return cachedDelegate.get().fuzzySearchTypes(searchTerm, typeFilter); + } + + @Override + public Flux> fuzzySearchPackages(String searchTerm) { + return cachedDelegate.get().fuzzySearchPackages(searchTerm); + } + + @Override + public Flux allSubtypesOf(IType type) { + return cachedDelegate.get().allSubtypesOf(type); + } + + protected ClasspathData createClasspathData() { + T newDelegate = delegateCreator.get(); + cachedDelegate.set(newDelegate); + try { + LinkedHashSet classpathEntries = new LinkedHashSet<>(newDelegate.getClasspathEntries()); + return new ClasspathData(newDelegate.getName(), classpathEntries, new LinkedHashSet<>(newDelegate.getClasspathResources()), newDelegate.getOutputFolder()); + } catch (Exception e) { + Log.log(e); + return new ClasspathData(newDelegate.getName(), Collections.emptySet(), new LinkedHashSet<>(newDelegate.getClasspathResources()), newDelegate.getOutputFolder()); + } + } + +} diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java index a30663512..79722f8c3 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java @@ -12,7 +12,8 @@ package org.springframework.ide.vscode.commons.java; import java.nio.file.Path; import java.util.function.Predicate; -import java.util.stream.Stream; + +import com.google.common.collect.ImmutableList; import reactor.core.publisher.Flux; import reactor.util.function.Tuple2; @@ -46,12 +47,12 @@ public interface IClasspath { * @return collection of classpath entries in a form file/folder paths * @throws Exception */ - Stream getClasspathEntries() throws Exception; + ImmutableList getClasspathEntries() throws Exception; /** * Classpath resources paths relative to the source folder path * @return classpath resource relative paths */ - Stream getClasspathResources(); - + ImmutableList getClasspathResources(); + } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java index 1b9631e3e..06c2e3b31 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java @@ -14,6 +14,8 @@ import org.springframework.ide.vscode.commons.javadoc.IJavadoc; public interface IJavaProject extends IJavaElement { + final static String PROJECT_CACHE_FOLDER = ".sts4-cache"; + IClasspath getClasspath(); @Override diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java index 2512338ff..abd2f3da5 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java @@ -11,14 +11,16 @@ package org.springframework.ide.vscode.commons.languageserver.java; import java.io.File; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.FileObserver; /** - * Cache fo java projects. The key for the cache is a "project" specific file + * Cache for java projects. The key for the cache is a "project" specific file * * @author Alex Boyko * @@ -28,19 +30,26 @@ public abstract class AbstractFileToProjectCache

    extends private String changeSubscription; private String deleteSubscription; - - public AbstractFileToProjectCache(FileObserver fileObserver) { + protected boolean asyncUpdate; + protected final Path projectCacheFolder; + private boolean alwaysFireEventOnFileChanged; + + public AbstractFileToProjectCache(FileObserver fileObserver, boolean asyncUpdate, Path projectCacheFolder) { super(fileObserver); + this.projectCacheFolder = projectCacheFolder; + this.asyncUpdate = asyncUpdate; + } + + + final public void setAlwaysFireEventOnFileChanged(boolean alwaysFireEventOnFileChanged) { + this.alwaysFireEventOnFileChanged = alwaysFireEventOnFileChanged; } @Override protected void attachListeners(File file, P project) { super.attachListeners(file, project); List globPattern = Arrays.asList(file.toString()); - changeSubscription = getFileObserver().onFileChanged(globPattern, (uri) -> { - update(project); - notifyProjectChanged(project); - }); + changeSubscription = getFileObserver().onFileChanged(globPattern, (uri) -> performUpdate(project, asyncUpdate)); deleteSubscription = getFileObserver().onFileDeleted(globPattern, (uri) -> { cache.invalidate(file); notifyProjectDeleted(project); @@ -49,6 +58,20 @@ public abstract class AbstractFileToProjectCache

    extends }); } - abstract protected void update(P project); + final protected void performUpdate(P project, boolean async) { + if (async) { + CompletableFuture.supplyAsync(() -> update(project)).thenAccept((changed) -> { + if (changed || alwaysFireEventOnFileChanged) { + notifyProjectChanged(project); + } + }); + } else { + if (update(project) || alwaysFireEventOnFileChanged) { + notifyProjectChanged(project); + } + } + } + + abstract protected boolean update(P project); } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java index 8e0728bb4..e2ace4ecc 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java @@ -11,8 +11,10 @@ package org.springframework.ide.vscode.commons.maven.java; import java.io.File; +import java.nio.file.Path; -import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.AbstractJavaProject; +import org.springframework.ide.vscode.commons.java.DelegatingCachedClasspath; import org.springframework.ide.vscode.commons.maven.MavenCore; /** @@ -21,21 +23,29 @@ import org.springframework.ide.vscode.commons.maven.MavenCore; * @author Alex Boyko * */ -public class MavenJavaProject implements IJavaProject { - - private MavenProjectClasspath classpath; - - public MavenJavaProject(MavenCore maven, File pom) { - this.classpath = new MavenProjectClasspath(maven, pom); +public class MavenJavaProject extends AbstractJavaProject { + + private DelegatingCachedClasspath classpath; + + public MavenJavaProject(MavenCore maven, File pom, Path projectDataCache) { + super(projectDataCache); + this.classpath = new DelegatingCachedClasspath<>( + () -> new MavenProjectClasspath(maven, pom), + projectDataCache == null ? null : projectDataCache.resolve(DelegatingCachedClasspath.CLASSPATH_DATA_CACHE_FILE).toFile() + ); } - + + public MavenJavaProject(MavenCore maven, File pom) { + this(maven, pom, null); + } + @Override - public MavenProjectClasspath getClasspath() { + public DelegatingCachedClasspath getClasspath() { return classpath; } - void update(MavenCore maven) { - this.classpath = new MavenProjectClasspath(maven, classpath.getPomFile()); + boolean update() { + return classpath.update(); } - + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java index bae929e28..227964cb4 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.maven.java; import java.io.File; +import java.nio.file.Path; import org.springframework.ide.vscode.commons.languageserver.java.AbstractFileToProjectCache; import org.springframework.ide.vscode.commons.maven.MavenCore; @@ -25,19 +26,24 @@ public class MavenProjectCache extends AbstractFileToProjectCache getClasspathEntries() throws Exception { + public ImmutableList getClasspathEntries() throws Exception { // return Stream.concat(maven.resolveDependencies(project, null).stream().map(artifact -> { // return artifact.getFile().toPath(); // }), projectResolvedOutput()); - return Stream.concat(projectDependencies().stream().map(a -> a.getFile().toPath()), projectOutput().stream().map(f -> f.toPath())); + ImmutableList classpathEntries = ImmutableList.copyOf(Stream.concat(projectDependencies().stream().map(a -> a.getFile().toPath()), + projectOutput().stream().map(f -> f.toPath())).collect(Collectors.toList())); + return classpathEntries; } private Set projectDependencies() { @@ -123,12 +131,12 @@ public class MavenProjectClasspath extends JandexClasspath { } @Override - public Stream getClasspathResources() { + public ImmutableList getClasspathResources() { MavenProject project = projectSupplier.get(); if (project == null) { - return Stream.empty(); + return ImmutableList.of(); } - return project.getBuild().getResources().stream().flatMap(resource -> { + return ImmutableList.copyOf(project.getBuild().getResources().stream().filter(resource -> new File(resource.getDirectory()).exists()).flatMap(resource -> { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(resource.getDirectory()); if (resource.getIncludes() != null && !resource.getIncludes().isEmpty()) { @@ -140,7 +148,7 @@ public class MavenProjectClasspath extends JandexClasspath { scanner.setCaseSensitive(false); scanner.scan(); return Arrays.stream(scanner.getIncludedFiles()); - }); + }).toArray(String[]::new)); } /* @@ -253,4 +261,20 @@ public class MavenProjectClasspath extends JandexClasspath { } } + @Override + public boolean equals(Object obj) { + if (obj instanceof MavenProjectClasspath) { + MavenProjectClasspath other = (MavenProjectClasspath) obj; + try { + if (pom.equals(other.pom) + && projectSupplier.get().equals(other.projectSupplier.get())) { + return super.equals(obj); + } + } catch (Throwable t) { + Log.log(t); + } + } + return false; + } + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java index 2b2157ebe..ea9f8da65 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java @@ -13,12 +13,15 @@ package org.springframework.ide.vscode.commons.maven.java.classpathfile; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IType; import org.springframework.ide.vscode.commons.maven.MavenCore; +import com.google.common.collect.ImmutableList; + import reactor.core.publisher.Flux; import reactor.util.function.Tuple2; @@ -37,15 +40,15 @@ public class FileClasspath implements IClasspath { } @Override - public Stream getClasspathEntries() throws Exception { - return Stream.concat(MavenCore.readClassPathFile(classpathFilePath), + public ImmutableList getClasspathEntries() throws Exception { + return ImmutableList.copyOf(Stream.concat(MavenCore.readClassPathFile(classpathFilePath), Stream.of(classpathFilePath.getParent().resolve("target/classes"), - classpathFilePath.getParent().resolve("target/test-classes"))); + classpathFilePath.getParent().resolve("target/test-classes"))).collect(Collectors.toList())); } @Override - public Stream getClasspathResources() { - return Stream.empty(); + public ImmutableList getClasspathResources() { + return ImmutableList.of(); } @Override diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java index 7e764ce8c..a48565f6f 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.commons.maven.java.classpathfile; import java.io.File; import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -67,8 +68,18 @@ public class JavaProjectWithClasspathFile implements IJavaProject { return true; } - void update() { - this.classpath = new FileClasspath(Paths.get(cpFile.toURI())); + CompletableFuture update() { + return CompletableFuture.supplyAsync(() -> doUpdate()); + } + + private synchronized boolean doUpdate() { + FileClasspath newClasspath = new FileClasspath(Paths.get(cpFile.toURI())); + if (newClasspath.equals(classpath)) { + return false; + } else { + this.classpath = newClasspath; + return true; + } } } \ No newline at end of file diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java index 95fc1897e..6e459ffe9 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java @@ -18,12 +18,13 @@ import org.springframework.ide.vscode.commons.util.FileObserver; public class JavaProjectWithClasspathFileCache extends AbstractFileToProjectCache { public JavaProjectWithClasspathFileCache(FileObserver fileObserver) { - super(fileObserver); + super(fileObserver, false, null); } @Override - protected void update(JavaProjectWithClasspathFile project) { - project.update(); + protected boolean update(JavaProjectWithClasspathFile project) { + project.update(); + return true; } @Override diff --git a/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java b/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java index b30302b1a..4a419fe19 100644 --- a/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java +++ b/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java @@ -12,11 +12,16 @@ package org.springframework.ide.vscode.commons.maven; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; +import org.assertj.core.util.Files; import org.junit.Test; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener; @@ -24,6 +29,8 @@ import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; import org.springframework.ide.vscode.commons.maven.java.MavenProjectCache; import org.springframework.ide.vscode.commons.util.BasicFileObserver; +import com.google.common.collect.ImmutableList; + /** * Tests for {@link MavenProjectCache} * @@ -31,38 +38,66 @@ import org.springframework.ide.vscode.commons.util.BasicFileObserver; * */ public class MavenProjectManagerTest { - + + private static void writeContent(File file, String content) throws IOException { + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.write(content); + } finally { + writer.close(); + } + } + @Test public void testPomFileChanges() throws Exception { Path testProjectPath = Paths.get(DependencyTreeTest.class.getResource("/empty-boot-project-with-classpath-file").toURI()); File pomFile = testProjectPath.resolve(MavenCore.POM_XML).toFile(); - BasicFileObserver fileObserver = new BasicFileObserver(); - MavenProjectCache cache = new MavenProjectCache(fileObserver, MavenCore.getDefault()); - IJavaProject[] projectChanged = new IJavaProject[] { null }; - IJavaProject[] projectDeleted = new IJavaProject[] { null }; - cache.addListener(new Listener() { - @Override - public void created(IJavaProject project) {} + + String pomFileContents = Files.contentOf(pomFile, Charset.defaultCharset()); - @Override - public void changed(IJavaProject project) { - projectChanged[0] = project; - } - @Override - public void deleted(IJavaProject project) { - projectDeleted[0] = project; - } - }); - - // Get the project from cache - MavenJavaProject cachedProject = cache.project(pomFile); - assertNotNull(cachedProject); - - fileObserver.notifyFileChanged(pomFile.toURI().toString()); - assertEquals(cachedProject, projectChanged[0]); - - fileObserver.notifyFileDeleted(pomFile.toURI().toString()); - assertEquals(cachedProject, projectDeleted[0]); + try { + BasicFileObserver fileObserver = new BasicFileObserver(); + MavenProjectCache cache = new MavenProjectCache(fileObserver, MavenCore.getDefault(), false, null); + IJavaProject[] projectChanged = new IJavaProject[] { null }; + IJavaProject[] projectDeleted = new IJavaProject[] { null }; + cache.addListener(new Listener() { + @Override + public void created(IJavaProject project) {} + + @Override + public void changed(IJavaProject project) { + projectChanged[0] = project; + } + @Override + public void deleted(IJavaProject project) { + projectDeleted[0] = project; + } + }); + + // Get the project from cache + MavenJavaProject cachedProject = cache.project(pomFile); + assertNotNull(cachedProject); + + ImmutableList calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(48, calculatedClassPath.size()); + + fileObserver.notifyFileChanged(pomFile.toURI().toString()); + assertNull(projectChanged[0]); + + writeContent(pomFile, Files.contentOf(testProjectPath.resolve("pom.newxml").toFile(), Charset.defaultCharset())); + fileObserver.notifyFileChanged(pomFile.toURI().toString()); + assertNotNull(projectChanged[0]); + assertEquals(cachedProject, projectChanged[0]); + calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(49, calculatedClassPath.size()); + + fileObserver.notifyFileDeleted(pomFile.toURI().toString()); + assertEquals(cachedProject, projectDeleted[0]); + } finally { + //restore original content + writeContent(pomFile, pomFileContents); + } } diff --git a/headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml b/headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml new file mode 100644 index 000000000..86392defd --- /dev/null +++ b/headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml @@ -0,0 +1,72 @@ + + + 4.0.0 + + com.example + empty-boot-1.4.0-web-app + 0.0.1-SNAPSHOT + jar + + empty-boot-project-with-classpath-file + Empty Boot project generating classpath.txt file + + + org.springframework.boot + spring-boot-starter-parent + 1.4.1.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-devtools + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-classpath + generate-sources + + build-classpath + + + + + classpath.txt + + + + + + +