diff --git a/eclipse-distribution/org.springframework.boot.ide.branding/makeWinIcon.sh b/eclipse-distribution/org.springframework.boot.ide.branding/makeWinIcon.sh new file mode 100644 index 000000000..1a75a6986 --- /dev/null +++ b/eclipse-distribution/org.springframework.boot.ide.branding/makeWinIcon.sh @@ -0,0 +1,40 @@ +# WARNING this script doesn't work, although it seems like it should. +# The equivalent of this script works on windows imagemagick. +# See makeWinIcon.bat + +# Althoug it doesn't work committing this file to git anyway +# for future reference. Maybe it can be fixed. + +#Shell script to convert a image into win icon format. The script uses +# unix commandline tool 'imagemagick' and the 'specs' for the images to +# insert into the .ico file are as follows (according to Martin Lippert) +# +# - the 16x, 32x, and 48x needs to be in the file twice - with 32bit color and with 8bit color +# - the 256x icon has to be 32bit only, but has to be uncompressed + +# code from here: https://github.com/neo4j-contrib/neoclipse/pull/56 + +# This command can be used to check whether the contents of the .ico file +# looks ok: +# +# identify -format '%f %p/%n %m %C/%Q %r %G %A %z\n' sts.ico + +convert sts256.png -compress none \ + \( -clone 0 -resize 16x16 -compress none \) \ + \( -clone 0 -resize 24x24 -compress none \) \ + \( -clone 0 -resize 32x32 -compress none \) \ + \( -clone 0 -resize 48x48 -compress none \) \ + \( -clone 0 -resize 16x16 -colors 256 -compress none \) \ + \( -clone 0 -resize 24x24 -colors 256 -compress none \) \ + \( -clone 0 -resize 32x32 -colors 256 -compress none \) \ + \( -clone 0 -resize 48x48 -colors 256 -compress none \) \ + \( -clone 0 -resize 256x256 -compress none \) \ + -delete 0 sts.ico + +convert sts512.png -compress none \ + \( -clone 0 -resize 256x256 -compress none \) \ + -delete 0 sts.ico + +convert sts256.png -compress none sts.ico + +identify -format '%f %p/%n %m %C/%Q %r %G %A %z\n' sts.ico diff --git a/eclipse-distribution/org.springframework.boot.ide.branding/sts4.ico b/eclipse-distribution/org.springframework.boot.ide.branding/sts4.ico index 2c0cad1ce..4985ead72 100644 Binary files a/eclipse-distribution/org.springframework.boot.ide.branding/sts4.ico and b/eclipse-distribution/org.springframework.boot.ide.branding/sts4.ico differ diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java index 16cdb76ea..88255051d 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java @@ -237,6 +237,8 @@ public class GotoSymbolDialog extends PopupDialog { // } // }); pattern.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + pattern.setMessage(model.getSearchBoxHintMessage()); + SwtConnect.connect(pattern, model.getSearchBox()); TreeViewer viewer = new TreeViewer(dialogArea, SWT.SINGLE); diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java index 58fc81902..562ddef1f 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java @@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableSet; public class GotoSymbolDialogModel { + private static final String SEARCH_BOX_HINT_MESSAGE = "@/ -> request mappings, @+ -> beans, @> -> functions, @ -> all spring elements"; private static final boolean DEBUG = false;//(""+Platform.getLocation()).contains("kdvolder"); private static void debug(String string) { if (DEBUG) { @@ -97,6 +98,10 @@ public class GotoSymbolDialogModel { dependsOn(unfilteredSymbols); } + private boolean containsCharactersCaseInsensitive(String symbol, String query) { + return containsCharacters(symbol.toLowerCase().toCharArray(), query.toLowerCase().toCharArray()); + } + private boolean containsCharacters(char[] symbolChars, char[] queryChars) { int symbolindex = 0; int queryindex = 0; @@ -113,9 +118,8 @@ public class GotoSymbolDialogModel { @Override protected ImmutableSet compute() { - char[] query = searchBox.getValue().toCharArray(); ImmutableSet.Builder builder = ImmutableSet.builder(); - unfilteredSymbols.getValues().stream().filter(sym -> containsCharacters(sym.getName().toCharArray(), query)).forEach(builder::add); + unfilteredSymbols.getValues().stream().filter(sym -> containsCharactersCaseInsensitive(sym.getName(), searchBox.getValue())).forEach(builder::add); return builder.build(); } }; @@ -149,6 +153,10 @@ public class GotoSymbolDialogModel { public LiveVariable getSearchBox() { return searchBox; } + + public String getSearchBoxHintMessage() { + return SEARCH_BOX_HINT_MESSAGE; + } public LiveExpression getStatus() { return status; 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/conditionals/ConditionalsLiveHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java index d8db7c1f7..f938b1427 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java @@ -140,18 +140,18 @@ public class ConditionalsLiveHoverProvider implements HoverProvider { // Check that Java type in annotation in editor matches Java information in the live Conditional ASTNode parent = annotation.getParent(); - String rawJsonKey = liveConditional.getPositiveMatchKey(); + String typeInfo = liveConditional.getTypeInfo(); if (parent instanceof MethodDeclaration) { MethodDeclaration methodDec = (MethodDeclaration) parent; IMethodBinding binding = methodDec.resolveBinding(); String annotationDeclaringClassName = binding.getDeclaringClass().getName(); String annotationMethodName = binding.getName(); - return rawJsonKey.contains(annotationDeclaringClassName) && rawJsonKey.contains(annotationMethodName); + return typeInfo.contains(annotationDeclaringClassName) && typeInfo.contains(annotationMethodName); } else if (parent instanceof TypeDeclaration) { TypeDeclaration typeDec = (TypeDeclaration) parent; String annotationDeclaringClassName = typeDec.resolveBinding().getName(); - return rawJsonKey.contains(annotationDeclaringClassName); + return typeInfo.contains(annotationDeclaringClassName); } return false; } 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 bca9a6c70..50e436435 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 @@ -110,7 +110,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 2733e8c1c..f164d1e45 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 @@ -55,6 +55,7 @@ import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener; import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -242,7 +243,7 @@ public class SpringIndexer { try { initializeTask.get(); return allsymbols.stream() - .filter(symbol -> containsCharacters(symbol.getName().toCharArray(), query.toCharArray())) + .filter(symbol -> StringUtil.containsCharactersCaseInsensitive(symbol.getName(), query)) .collect(Collectors.toList()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); @@ -251,20 +252,6 @@ public class SpringIndexer { return null; } - private boolean containsCharacters(char[] symbolChars, char[] queryChars) { - int symbolindex = 0; - int queryindex = 0; - - while (queryindex < queryChars.length && symbolindex < symbolChars.length) { - if (symbolChars[symbolindex] == queryChars[queryindex]) { - queryindex++; - } - symbolindex++; - } - - return queryindex == queryChars.length; - } - private void scanFiles(File directory) { try { Map, List> projects = Files.walk(directory.toPath()) @@ -456,7 +443,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/boot/java/conditionals/test/ConditionalsLiveHoverTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java index f603e7122..0c9a755f5 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java @@ -69,7 +69,7 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("22022").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"ConditionalOnBeanConfig#hi\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found bean 'missing'\"}]}}") .build(); @@ -93,7 +93,7 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("22022").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"ConditionalOnMissingBeanConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}]}}") .build(); @@ -116,7 +116,7 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("22022").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"HelloConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}],\"HelloConfig2#hi\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found bean 'missing'\"}],\"MultipleConditionals#hi\":[{\"condition\":\"OnClassCondition\",\"message\":\"@ConditionalOnClass found required class; @ConditionalOnMissingClass did not find unwanted class\"},{\"condition\":\"OnWebApplicationCondition\",\"message\":\"@ConditionalOnWebApplication (required) found StandardServletEnvironment\"},{\"condition\":\"OnJavaCondition\",\"message\":\"@ConditionalOnJava (1.8 or newer) found 1.8\"},{\"condition\":\"OnExpressionCondition\",\"message\":\"@ConditionalOnExpression (#{true}) resulted in true\"},{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found beans 'hi', 'missing'\"}]}}") .build(); @@ -158,19 +158,19 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1000").processId("70000").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"ConditionalOnMissingBeanConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}]}}") .build(); mockAppProvider.builder().isSpringBootApp(true).port("1001").processId("80000").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"ConditionalOnMissingBeanConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}]}}") .build(); mockAppProvider.builder().isSpringBootApp(true).port("1002").processId("90000").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"ConditionalOnMissingBeanConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}]}}") .build(); @@ -218,7 +218,7 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1000").processId("70000").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"HelloConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}],\"HelloConfig2#hi\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found bean 'missing'\"}],\"MultipleConditionals#hi\":[{\"condition\":\"OnClassCondition\",\"message\":\"@ConditionalOnClass found required class; @ConditionalOnMissingClass did not find unwanted class\"},{\"condition\":\"OnWebApplicationCondition\",\"message\":\"@ConditionalOnWebApplication (required) found StandardServletEnvironment\"},{\"condition\":\"OnJavaCondition\",\"message\":\"@ConditionalOnJava (1.8 or newer) found 1.8\"},{\"condition\":\"OnExpressionCondition\",\"message\":\"@ConditionalOnExpression (#{true}) resulted in true\"},{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found beans 'hi', 'missing'\"}]}}") .build(); @@ -264,7 +264,7 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1000").processId("70000").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"MultipleConditionalsPT152535713#hi\":[{\"condition\":\"OnWebApplicationCondition\",\"message\":\"@ConditionalOnWebApplication (required) found StandardServletEnvironment\"},{\"condition\":\"OnJavaCondition\",\"message\":\"@ConditionalOnJava (1.8 or newer) found 1.8\"}]}}") .build(); @@ -290,7 +290,7 @@ public class ConditionalsLiveHoverTest { } @Test - public void testHighlights() throws Exception { + public void testHighlightsMethodConditionals() throws Exception { File directory = new File( ProjectsHarness.class.getResource("/test-projects/test-conditionals-live-hover/").toURI()); @@ -299,18 +299,131 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("22022").host("cfapps.io") .processName("test-conditionals-live-hover") - .positiveMatchesJsonForLiveConditionals( + .liveConditionalsJson( "{\"positiveMatches\":{\"HelloConfig#missing\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\"}],\"HelloConfig2#hi\":[{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found bean 'missing'\"}],\"MultipleConditionals#hi\":[{\"condition\":\"OnClassCondition\",\"message\":\"@ConditionalOnClass found required class; @ConditionalOnMissingClass did not find unwanted class\"},{\"condition\":\"OnWebApplicationCondition\",\"message\":\"@ConditionalOnWebApplication (required) found StandardServletEnvironment\"},{\"condition\":\"OnJavaCondition\",\"message\":\"@ConditionalOnJava (1.8 or newer) found 1.8\"},{\"condition\":\"OnExpressionCondition\",\"message\":\"@ConditionalOnExpression (#{true}) resulted in true\"},{\"condition\":\"OnBeanCondition\",\"message\":\"@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found beans 'hi', 'missing'\"}]}}") .build(); harness.intialize(directory); + String content = "package example;\n" + + "\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnJava;\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "\n" + + "@Configuration\n" + + "public class MultipleConditionals {\n" + + "\n" + + " @Bean\n" + + " @ConditionalOnBean\n" + + " @ConditionalOnWebApplication\n" + + " @ConditionalOnJava(value=ConditionalOnJava.JavaVersion.EIGHT)\n" + + " @ConditionalOnMissingClass\n" + + " @ConditionalOnExpression\n" + + " public Hello hi() {\n" + + " return null;\n" + + " }\n" + + "}"; - Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); + Editor editor = harness.newEditor( LanguageId.JAVA, content, docUri); editor.assertHighlights("@ConditionalOnBean", "@ConditionalOnWebApplication", "@ConditionalOnJava(value=ConditionalOnJava.JavaVersion.EIGHT)", "@ConditionalOnMissingClass", "@ConditionalOnExpression"); } + + @Test + public void testHighlightsTypeConditionals() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-conditionals-live-hover/").toURI()); + String docUri = "file://" + directory.getAbsolutePath() + "/src/main/java/example/MultipleConditionals.java"; + + // Build a mock running boot app + mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("22022").host("cfapps.io") + .processName("test-conditionals-live-hover") + .liveConditionalsJson( + "{\"negativeMatches\": {\n" + + " \"MyConditionalComponent\": {\n" + + " \"notMatched\": [\n" + + " {\n" + + " \"condition\": \"OnClassCondition\",\n" + + " \"message\": \"@ConditionalOnClass did not find required class 'java.lang.String2'\"\n" + + " }\n" + + " ],\n" + + " \"matched\": []\n" + + " }\n" + + "}\n" + + "}") + .build(); + + harness.intialize(directory); + + String content = "package com.example;\n" + + "\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "@ConditionalOnClass(name=\"java.lang.String2\")\n" + + "public class MyConditionalComponent {\n" + + "}"; + + Editor editor = harness.newEditor( LanguageId.JAVA, content, docUri); + + editor.assertHighlights("@ConditionalOnClass(name=\"java.lang.String2\")"); + + } + + @Test + public void testNegativeMatches() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-conditionals-live-hover/").toURI()); + String docUri = "file://" + directory.getAbsolutePath() + "/src/main/java/example/MultipleConditionals.java"; + + // Build a mock running boot app + mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("67950").host("cfapps.io") + .processName("test-conditionals-live-hover") + .liveConditionalsJson( + "{\"negativeMatches\": {\n" + + " \"MyConditionalComponent\": {\n" + + " \"notMatched\": [\n" + + " {\n" + + " \"condition\": \"OnClassCondition\",\n" + + " \"message\": \"@ConditionalOnClass did not find required class 'java.lang.String2'\"\n" + + " }\n" + + " ],\n" + + " \"matched\": []\n" + + " }\n" + + "}\n" + + "}") + .build(); + + harness.intialize(directory); + + String content = "package com.example;\n" + + "\n" + + "import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "@ConditionalOnClass(name=\"java.lang.String2\")\n" + + "public class MyConditionalComponent {\n" + + "}"; + + Editor editor = harness.newEditor( LanguageId.JAVA, content, docUri); + + editor.assertHoverContains("@ConditionalOnClass(name=\"java.lang.String2\")", "@ConditionalOnClass did not find required class 'java.lang.String2'\n" + + "\n" + + "Process [PID=67950, name=`test-conditionals-live-hover`]"); + + } } 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-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java index f5c6aaade..fd4e58e76 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java @@ -111,12 +111,7 @@ public class MockRunningAppProvider { return this; } - public MockAppBuilder autoConfigReport(String autoConfigReport) throws Exception { - when(app.getAutoConfigReport()).thenReturn(autoConfigReport); - return this; - } - - public MockAppBuilder positiveMatchesJsonForLiveConditionals(String rawJson) throws Exception{ + public MockAppBuilder liveConditionalsJson(String rawJson) throws Exception{ when(app.getLiveConditionals()).thenReturn(SpringBootApp.getLiveConditionals(rawJson, processId, processName)); return this; } diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-conditionals-live-hover/src/main/java/example/MultipleConditionals.java b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-conditionals-live-hover/src/main/java/example/MultipleConditionals.java index 4417dc166..b4257ec43 100644 --- a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-conditionals-live-hover/src/main/java/example/MultipleConditionals.java +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-conditionals-live-hover/src/main/java/example/MultipleConditionals.java @@ -22,11 +22,4 @@ public class MultipleConditionals { public Hello hi() { return null; } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnNotWebApplication - public Hello missing() { - return null; - } } 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-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditional.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditional.java index 3f4432838..037c05841 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditional.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditional.java @@ -16,7 +16,7 @@ public class LiveConditional { private String message; private String processId; private String processName; - private String positiveMatchKey; + private String typeInfo; public LiveConditional() { @@ -38,8 +38,8 @@ public class LiveConditional { return processName; } - public String getPositiveMatchKey() { - return positiveMatchKey; + public String getTypeInfo() { + return typeInfo; } public static class LiveConditionalBuilder { @@ -67,13 +67,31 @@ public class LiveConditional { } /** - * This is a JSON key in "positiveMatches" element in the autoconfig report that contains information regarding - * the method that the conditional is applied to. - * @param positiveMatchKey + * Type information for which a conditional is applied to. + *

+ * + * Example: + *

+ * For this class: + *

+ * "@ConditionalOnClass(name="java.lang.String2") + * public class MyConditionalComponent { + * }" + *

+ * This is the "real" autoconfig JSON: + *

+ * "negativeMatches": { "MyConditionalComponent": { "notMatched": [ { + * "condition": "OnClassCondition", "message": "@ConditionalOnClass did not find + * required class 'java.lang.String2'" } ], "matched": [] } + *

+ * In this example, "MyConditionalComponent" information in the JSON indicates the type where the conditional is being applied to. + *

+ * Type info can also contain method information if a conditional annotation is applied to a method. Example: MyConditionalComponent#myBean) + * @param typeInfo * @return */ - public LiveConditionalBuilder positiveMatchKey(String positiveMatchKey) { - conditional.positiveMatchKey = positiveMatchKey; + public LiveConditionalBuilder typeInfo(String typeInfo) { + conditional.typeInfo = typeInfo; return this; } diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditionalParser.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditionalParser.java index e871989fd..af45b2956 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditionalParser.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/LiveConditionalParser.java @@ -50,6 +50,8 @@ public class LiveConditionalParser { if (StringUtil.hasText(autoConfigRecord)) { getConditionalsFromPositiveMatches(autoConfigRecord).stream() .forEach(conditional -> allConditionals.add(conditional)); + getConditionalsFromNegativeMatches(autoConfigRecord).stream() + .forEach(conditional -> allConditionals.add(conditional)); } if (!allConditionals.isEmpty()) { return Optional.of(allConditionals); @@ -61,9 +63,7 @@ public class LiveConditionalParser { } /** - * Fetches the "positiveMatches" element in the autoconfig report that contains conditional information. - * @param positiveMatchKey - * @return + * Fetches the "positiveMatches" element in the autoconfig report JSON that contains conditional information. */ private Optional getPositiveMatchesJson(String autoConfigReport) { JSONObject autoConfigJson = new JSONObject(autoConfigReport); @@ -83,46 +83,100 @@ public class LiveConditionalParser { return Optional.empty(); } + /** + * Fetches the "negativeMatches" element in the autoconfig report JSON that contains conditional information. + */ + private Optional getNegativeMatchesJson(String autoConfigReport) { + JSONObject autoConfigJson = new JSONObject(autoConfigReport); + + Iterator keys = autoConfigJson.keys(); + + while (keys.hasNext()) { + String key = keys.next(); + if ("negativeMatches".equals(key)) { + Object obj = autoConfigJson.get(key); + if (obj instanceof JSONObject) { + return Optional.of((JSONObject) obj); + } + } + } + + return Optional.empty(); + } + /** * Fetches all the conditionals listed in the the "positiveMatches" element in the autoconfig report. * */ - private List getConditionalsFromPositiveMatches(String autoconfigReport) { + private List getConditionalsFromPositiveMatches(String autoConfigReport) { List conditions = new ArrayList<>(); - - getPositiveMatchesJson(autoconfigReport).ifPresent((positiveMatches) -> { - Iterator pMKeys = positiveMatches.keys(); - while (pMKeys.hasNext()) { + getPositiveMatchesJson(autoConfigReport).ifPresent((matches) -> { + matches.keySet().stream().forEach(typeInfo -> { // The positive match key contains the bean method information where conditional // was applied to - String positiveMatchKey = pMKeys.next(); - JSONArray matchList = (JSONArray) positiveMatches.get(positiveMatchKey); - matchList.forEach((match) -> { - if (match instanceof JSONObject) { - resolveConditional(positiveMatchKey, (JSONObject) match) - .ifPresent((condition) -> conditions.add(condition)); - } - }); - } - }); + Object val = matches.get(typeInfo); + if (val instanceof JSONArray) { + JSONArray contentList = (JSONArray) val; + parseConditionalsFromContentList(conditions, typeInfo, contentList); + } + }); + }); return conditions; } - private Optional resolveConditional(String positiveMatchKey, JSONObject conditionalJson) { - if (conditionalJson != null) { - String condition = (String) conditionalJson.get("condition"); - String message = (String) conditionalJson.get("message"); - // We care about the message itself as it contains the actual annotation as well - // as the reason it matched - if (StringUtil.hasText(message)) { - return Optional.of(LiveConditional.builder().processId(appProcessId).processName(appProcessName) - .condition(condition).message(message).positiveMatchKey(positiveMatchKey).build()); - } - } - return Optional.empty(); + private List getConditionalsFromNegativeMatches(String autoConfigReport) { + List conditions = new ArrayList<>(); + // The JSON structure being parsed is: +// "negativeMatches": { +// "MyConditionalComponent": { +// "notMatched": [ +// { +// "condition": "OnClassCondition", +// "message": "@ConditionalOnClass did not find required class 'java.lang.String2'" +// } +// ], +// "matched": [] +// } + getNegativeMatchesJson(autoConfigReport).ifPresent((matches) -> { + // The key in the "matches" JSON contains the live type information where the conditional was applied to + matches.keySet().stream().forEach(typeInfo -> { + // The positive match key contains the bean method information where conditional + // was applied to + Object val = matches.get(typeInfo); + if (val instanceof JSONObject) { + JSONObject negativeMatches = (JSONObject) val; + negativeMatches.keySet().stream().forEach((key) -> { + JSONArray contentList = (JSONArray) negativeMatches.get(key); + parseConditionalsFromContentList(conditions, typeInfo, contentList); + }); + } + }); + + }); + return conditions; } + private void parseConditionalsFromContentList(List conditionals, String typeInfo, + JSONArray contentList) { + contentList.forEach((content) -> { + if (content instanceof JSONObject) { + JSONObject conditionalJson = (JSONObject) content; + String condition = (String) conditionalJson.get("condition"); + String message = (String) conditionalJson.get("message"); + // We care about the message itself as it contains the actual annotation as well + // as the reason it matched + if (StringUtil.hasText(message)) { + LiveConditional conditional = LiveConditional.builder().processId(appProcessId) + .processName(appProcessName).condition(condition).message(message).typeInfo(typeInfo) + .build(); + conditionals.add(conditional); + } + } + }); + } + + public static Optional> parse(String autoConfigRecord, String appProcessId, String appProcessName) { return new LiveConditionalParser(autoConfigRecord, appProcessId, appProcessName).parse(); diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java index 32aca0891..0a670bdd7 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java @@ -251,7 +251,7 @@ public class SpringBootApp { */ public static Optional> getLiveConditionals(String autoConfigReport, String processId, String processName) { - return new LiveConditionalParser(autoConfigReport, processId, processName).parse(); + return LiveConditionalParser.parse(autoConfigReport, processId, processName); } public String getAutoConfigReport() throws Exception { 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 + + + + + + + diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java index 2893bdea1..b5f4b3d47 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java @@ -228,4 +228,22 @@ public class StringUtil { } return fullyQualifiedName; } + + public static boolean containsCharactersCaseInsensitive(String value, String query) { + return containsCharacters(value.toLowerCase().toCharArray(), query.toLowerCase().toCharArray()); + } + + public static boolean containsCharacters(char[] valChars, char[] queryChars) { + int symbolindex = 0; + int queryindex = 0; + + while (queryindex < queryChars.length && symbolindex < valChars.length) { + if (valChars[symbolindex] == queryChars[queryindex]) { + queryindex++; + } + symbolindex++; + } + + return queryindex == queryChars.length; + } }