From 539afeca3a83871bea54ce4319062966c85d51ef Mon Sep 17 00:00:00 2001 From: nsingh Date: Tue, 14 Nov 2017 22:57:41 -0800 Subject: [PATCH 1/8] PT 152801833 - Added support for negative matches parsing for live conditionals --- .../ConditionalsLiveHoverProvider.java | 6 +- .../test/ConditionalsLiveHoverTest.java | 135 ++++++++++++++++-- .../harness/MockRunningAppProvider.java | 2 +- .../java/example/MultipleConditionals.java | 7 - .../commons/boot/app/cli/LiveConditional.java | 34 +++-- .../boot/app/cli/LiveConditionalParser.java | 114 +++++++++++---- .../commons/boot/app/cli/SpringBootApp.java | 2 +- 7 files changed, 239 insertions(+), 61 deletions(-) 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/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..2cf08d656 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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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( + .conditionalsJson( "{\"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") + .conditionalsJson( + "{\"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") + .conditionalsJson( + "{\"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/MockRunningAppProvider.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java index f5c6aaade..ba45ad460 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 @@ -116,7 +116,7 @@ public class MockRunningAppProvider { return this; } - public MockAppBuilder positiveMatchesJsonForLiveConditionals(String rawJson) throws Exception{ + public MockAppBuilder conditionalsJson(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/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 { From 5511552253e5677de5750c766615521e9028a5b2 Mon Sep 17 00:00:00 2001 From: nsingh Date: Tue, 14 Nov 2017 23:00:22 -0800 Subject: [PATCH 2/8] Renamed mock API --- .../test/ConditionalsLiveHoverTest.java | 22 +++++++++---------- .../harness/MockRunningAppProvider.java | 7 +----- 2 files changed, 12 insertions(+), 17 deletions(-) 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 2cf08d656..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") - .conditionalsJson( + .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") - .conditionalsJson( + .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") - .conditionalsJson( + .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") - .conditionalsJson( + .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") - .conditionalsJson( + .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") - .conditionalsJson( + .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") - .conditionalsJson( + .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") - .conditionalsJson( + .liveConditionalsJson( "{\"positiveMatches\":{\"MultipleConditionalsPT152535713#hi\":[{\"condition\":\"OnWebApplicationCondition\",\"message\":\"@ConditionalOnWebApplication (required) found StandardServletEnvironment\"},{\"condition\":\"OnJavaCondition\",\"message\":\"@ConditionalOnJava (1.8 or newer) found 1.8\"}]}}") .build(); @@ -299,7 +299,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") - .conditionalsJson( + .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(); @@ -349,7 +349,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") - .conditionalsJson( + .liveConditionalsJson( "{\"negativeMatches\": {\n" + " \"MyConditionalComponent\": {\n" + " \"notMatched\": [\n" + @@ -392,7 +392,7 @@ public class ConditionalsLiveHoverTest { // Build a mock running boot app mockAppProvider.builder().isSpringBootApp(true).port("1111").processId("67950").host("cfapps.io") .processName("test-conditionals-live-hover") - .conditionalsJson( + .liveConditionalsJson( "{\"negativeMatches\": {\n" + " \"MyConditionalComponent\": {\n" + " \"notMatched\": [\n" + 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 ba45ad460..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 conditionalsJson(String rawJson) throws Exception{ + public MockAppBuilder liveConditionalsJson(String rawJson) throws Exception{ when(app.getLiveConditionals()).thenReturn(SpringBootApp.getLiveConditionals(rawJson, processId, processName)); return this; } From cee1005bb8f460042781c4dc30bd9c70014012c5 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 14 Nov 2017 12:06:26 -0500 Subject: [PATCH 3/8] PT #151547534 Cache classpath in a file --- .../java/BootJavaLanguageServerParams.java | 30 ++- .../handlers/BootJavaCompletionEngine.java | 2 +- .../java/handlers/BootJavaHoverProvider.java | 2 +- .../handlers/BootJavaReferencesHandler.java | 2 +- .../boot/java/utils/BootProjectUtil.java | 2 +- .../boot/java/utils/CompilationUnitCache.java | 2 +- .../vscode/boot/java/utils/SpringIndexer.java | 2 +- .../harness/BootLanguageServerHarness.java | 4 +- .../boot/BootPropertiesLanguageServer.java | 6 +- .../boot/metadata/ResourceHintProvider.java | 2 +- .../commons/gradle/GradleJavaProject.java | 30 +-- .../commons/gradle/GradleProjectCache.java | 17 +- .../gradle/GradleProjectClasspath.java | 112 +++++--- .../commons/gradle/GradleProjectTest.java | 100 ++++--- .../empty-gradle-project/build.newgradle | 34 +++ .../commons/commons-java/pom.xml | 5 + .../commons/jandex/JandexClasspath.java | 2 +- .../commons/java/AbstractJavaProject.java | 29 ++ .../java/DelegatingCachedClasspath.java | 247 ++++++++++++++++++ .../ide/vscode/commons/java/IClasspath.java | 9 +- .../ide/vscode/commons/java/IJavaProject.java | 2 + .../java/AbstractFileToProjectCache.java | 39 ++- .../commons/maven/java/MavenJavaProject.java | 34 ++- .../commons/maven/java/MavenProjectCache.java | 16 +- .../maven/java/MavenProjectClasspath.java | 36 ++- .../java/classpathfile/FileClasspath.java | 13 +- .../JavaProjectWithClasspathFile.java | 15 +- .../JavaProjectWithClasspathFileCache.java | 7 +- .../maven/MavenProjectManagerTest.java | 89 +++++-- .../pom.newxml | 72 +++++ 30 files changed, 786 insertions(+), 176 deletions(-) create mode 100644 headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle create mode 100644 headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java create mode 100644 headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java create mode 100644 headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java index ca7a8afba..721f5add3 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerParams.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java; +import java.nio.file.Paths; import java.time.Duration; import java.util.Arrays; @@ -21,6 +22,7 @@ import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.commons.gradle.GradleCore; import org.springframework.ide.vscode.commons.gradle.GradleProjectCache; import org.springframework.ide.vscode.commons.gradle.GradleProjectFinder; +import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.CompositeJavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.java.CompositeProjectOvserver; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -60,10 +62,10 @@ public class BootJavaLanguageServerParams { // Initialize project finders, project caches and project observers FileObserver fileObserver = server.getWorkspaceService().getFileObserver(); CompositeJavaProjectFinder javaProjectFinder = new CompositeJavaProjectFinder(); - MavenProjectCache mavenProjectCache = new MavenProjectCache(fileObserver, MavenCore.getDefault()); + MavenProjectCache mavenProjectCache = new MavenProjectCache(fileObserver, MavenCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new MavenProjectFinder(mavenProjectCache)); - GradleProjectCache gradleProjectCache = new GradleProjectCache(fileObserver, GradleCore.getDefault()); + GradleProjectCache gradleProjectCache = new GradleProjectCache(fileObserver, GradleCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new GradleProjectFinder(gradleProjectCache)); CompositeProjectOvserver projectObserver = new CompositeProjectOvserver(Arrays.asList(mavenProjectCache, gradleProjectCache)); @@ -78,5 +80,29 @@ public class BootJavaLanguageServerParams { }; } + public static LSFactory createTestDefault() { + return (SimpleLanguageServer server) -> { + // Initialize project finders, project caches and project observers + FileObserver fileObserver = server.getWorkspaceService().getFileObserver(); + CompositeJavaProjectFinder javaProjectFinder = new CompositeJavaProjectFinder(); + MavenProjectCache mavenProjectCache = new MavenProjectCache(fileObserver, MavenCore.getDefault(), false, null); + mavenProjectCache.setAlwaysFireEventOnFileChanged(true); + javaProjectFinder.addJavaProjectFinder(new MavenProjectFinder(mavenProjectCache)); + + GradleProjectCache gradleProjectCache = new GradleProjectCache(fileObserver, GradleCore.getDefault(), false, null); + gradleProjectCache.setAlwaysFireEventOnFileChanged(true); + javaProjectFinder.addJavaProjectFinder(new GradleProjectFinder(gradleProjectCache)); + + CompositeProjectOvserver projectObserver = new CompositeProjectOvserver(Arrays.asList(mavenProjectCache, gradleProjectCache)); + + return new BootJavaLanguageServerParams( + javaProjectFinder.filter(BootProjectUtil::isBootProject), + projectObserver, + new DefaultSpringPropertyIndexProvider(javaProjectFinder, projectObserver), + RunningAppProvider.DEFAULT, + SpringLiveHoverWatchdog.DEFAULT_INTERVAL + ); + }; + } } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java index 214e65303..e93946ab8 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java @@ -111,7 +111,7 @@ public class BootJavaCompletionEngine implements ICompletionEngine { private String[] getClasspathEntries(IDocument doc) throws Exception { IJavaProject project = this.projectFinder.find(new TextDocumentIdentifier(doc.getUri())).get(); IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index 51099c297..ca5dcd0a5 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -213,7 +213,7 @@ public class BootJavaHoverProvider implements HoverHandler { try { IClasspath classpath = project.getClasspath(); if (classpath!=null) { - return classpath.getClasspathEntries().anyMatch(cpe -> { + return classpath.getClasspathEntries().stream().anyMatch(cpe -> { String name = cpe.getFileName().toString(); return name.startsWith("spring-boot-actuator-"); }); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java index a325aee1f..2e032aa7e 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java @@ -126,7 +126,7 @@ public class BootJavaReferencesHandler implements ReferencesHandler { private String[] getClasspathEntries(IDocument doc) throws Exception { IJavaProject project = this.projectFinder.find(new TextDocumentIdentifier(doc.getUri())).get(); IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java index 9994c7da6..dd3272316 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/BootProjectUtil.java @@ -22,7 +22,7 @@ public class BootProjectUtil { try { IClasspath cp = jp.getClasspath(); if (cp!=null) { - return cp.getClasspathEntries().anyMatch(cpe -> isBootEntry(cpe)); + return cp.getClasspathEntries().stream().anyMatch(cpe -> isBootEntry(cpe)); } } catch (Exception e) { Log.log(e); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index 4be7e4429..776c638d1 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -133,7 +133,7 @@ public final class CompilationUnitCache { return new String[0]; } else { IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java index 2c423a083..811d93b2c 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java @@ -456,7 +456,7 @@ public class SpringIndexer { private String[] getClasspathEntries(IJavaProject project) throws Exception { IClasspath classpath = project.getClasspath(); - Stream classpathEntries = classpath.getClasspathEntries(); + Stream classpathEntries = classpath.getClasspathEntries().stream(); return classpathEntries .filter(path -> path.toFile().exists()) .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java index 6c68d7772..997df25de 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/BootLanguageServerHarness.java @@ -43,7 +43,7 @@ public class BootLanguageServerHarness extends LanguageServerHarness defaultsFactory = BootJavaLanguageServerParams.createDefault(); + LSFactory defaultsFactory = BootJavaLanguageServerParams.createTestDefault(); private JavaProjectFinder projectFinder = null; private ProjectObserver projectObserver = null; private SpringPropertyIndexProvider indexProvider = null; @@ -92,7 +92,7 @@ public class BootLanguageServerHarness extends LanguageServerHarness { LSFactory params = (server) -> { - BootJavaLanguageServerParams defaults = BootJavaLanguageServerParams.createDefault().create(server); + BootJavaLanguageServerParams defaults = BootJavaLanguageServerParams.createTestDefault().create(server); return new BootJavaLanguageServerParams( builder.projectFinder==null?defaults.projectFinder:builder.projectFinder, builder.projectObserver==null?defaults.projectObserver:builder.projectObserver, diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java index 422915941..e0983655d 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/BootPropertiesLanguageServer.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot; +import java.nio.file.Paths; import java.util.Arrays; import org.springframework.ide.vscode.boot.common.PropertyCompletionFactory; @@ -25,6 +26,7 @@ import org.springframework.ide.vscode.boot.yaml.reconcile.ApplicationYamlReconci import org.springframework.ide.vscode.commons.gradle.GradleCore; import org.springframework.ide.vscode.commons.gradle.GradleProjectCache; import org.springframework.ide.vscode.commons.gradle.GradleProjectFinder; +import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionEngine; import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter; import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider; @@ -120,10 +122,10 @@ public class BootPropertiesLanguageServer extends SimpleLanguageServer { documents.onHover(hoverEngine::getHover); // Initialize project finders, project caches and project observers - MavenProjectCache mavenProjectCache = new MavenProjectCache(getWorkspaceService().getFileObserver(), MavenCore.getDefault()); + MavenProjectCache mavenProjectCache = new MavenProjectCache(getWorkspaceService().getFileObserver(), MavenCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new MavenProjectFinder(mavenProjectCache)); - GradleProjectCache gradleProjectCache = new GradleProjectCache(getWorkspaceService().getFileObserver(), GradleCore.getDefault()); + GradleProjectCache gradleProjectCache = new GradleProjectCache(getWorkspaceService().getFileObserver(), GradleCore.getDefault(), true, Paths.get(IJavaProject.PROJECT_CACHE_FOLDER)); javaProjectFinder.addJavaProjectFinder(new GradleProjectFinder(gradleProjectCache)); projectObserver = new CompositeProjectOvserver(Arrays.asList(mavenProjectCache, gradleProjectCache)); diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java index beb3891ee..bb630ce8f 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/ResourceHintProvider.java @@ -63,7 +63,7 @@ public class ResourceHintProvider implements ValueProviderStrategy { private static class ClasspathHints extends CachingValueProvider { @Override protected Flux getValuesAsync(IJavaProject javaProject, String query) { - return Flux.fromStream(javaProject.getClasspath().getClasspathResources().distinct().map(StsValueHint::create)); + return Flux.fromStream(javaProject.getClasspath().getClasspathResources().stream().distinct().map(StsValueHint::create)); } } diff --git a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java index 03596a937..474529bc0 100644 --- a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java +++ b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleJavaProject.java @@ -11,9 +11,10 @@ package org.springframework.ide.vscode.commons.gradle; import java.io.File; +import java.nio.file.Path; -import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.java.AbstractJavaProject; +import org.springframework.ide.vscode.commons.java.DelegatingCachedClasspath; /** * Implementation of Gradle Java project @@ -21,16 +22,18 @@ import org.springframework.ide.vscode.commons.util.Log; * @author Alex Boyko * */ -public class GradleJavaProject implements IJavaProject { +public class GradleJavaProject extends AbstractJavaProject { - private GradleCore gradle; - private GradleProjectClasspath classpath; + private DelegatingCachedClasspath classpath; private File projectDir; - public GradleJavaProject(GradleCore gradle, File projectDir) throws GradleException { - this.gradle = gradle; + public GradleJavaProject(GradleCore gradle, File projectDir, Path projectDataCache) { + super(projectDataCache); this.projectDir = projectDir; - this.classpath = new GradleProjectClasspath(gradle, projectDir); + this.classpath = new DelegatingCachedClasspath( + () -> new GradleProjectClasspath(gradle, projectDir), + projectDataCache == null ? null : projectDataCache.resolve(DelegatingCachedClasspath.CLASSPATH_DATA_CACHE_FILE).toFile() + ); } public File getLocation() { @@ -38,16 +41,13 @@ public class GradleJavaProject implements IJavaProject { } @Override - public GradleProjectClasspath getClasspath() { + public DelegatingCachedClasspath getClasspath() { return classpath; } - void update() { - try { - this.classpath = new GradleProjectClasspath(gradle, projectDir); - } catch (GradleException e) { - Log.log(e); - } + boolean update() { + return classpath.update(); } + } diff --git a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java index da2185273..94c9fbbb8 100644 --- a/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java +++ b/headless-services/commons/commons-gradle/src/main/java/org/springframework/ide/vscode/commons/gradle/GradleProjectCache.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.gradle; import java.io.File; +import java.nio.file.Path; import org.springframework.ide.vscode.commons.languageserver.java.AbstractFileToProjectCache; import org.springframework.ide.vscode.commons.util.FileObserver; @@ -25,19 +26,25 @@ public class GradleProjectCache extends AbstractFileToProjectCache gradleProject; private Supplier buildEnvironment; - public GradleProjectClasspath(GradleCore gradle, File projectDir) throws GradleException { + public GradleProjectClasspath(GradleCore gradle, File projectDir) { super(); - this.gradleProject = gradle.getModel(projectDir, EclipseProject.class); + this.gradleProject = Suppliers.memoize(() -> { + try { + return gradle.getModel(projectDir, EclipseProject.class); + } catch (GradleException e) { + Log.log(e); + return null; + } + }); this.buildEnvironment = Suppliers.memoize(() -> { try { return gradle.getModel(projectDir, BuildEnvironment.class); @@ -84,7 +92,10 @@ public class GradleProjectClasspath extends JandexClasspath { } public EclipseProject getRootProject() { - EclipseProject root = this.gradleProject; + EclipseProject root = this.gradleProject.get(); + if (root == null) { + return root; + } while(root.getParent() != null) { root = root.getParent(); } @@ -92,15 +103,21 @@ public class GradleProjectClasspath extends JandexClasspath { } @Override - public Stream getClasspathEntries() throws Exception { + public ImmutableList getClasspathEntries() throws Exception { EclipseProject root = getRootProject(); - return Stream.concat(gradleProject.getClasspath().stream().map(dep -> dep.getFile().toPath()), - gradleProject.getProjectDependencies().stream() - .map(d -> findPeer(root, d.getTargetProject().getName())) - .filter(o -> o.isPresent()) - .map(o -> o.get()) - .map(p -> p.getProjectDirectory().toPath().resolve(p.getOutputLocation().getPath())) - ); + EclipseProject project = gradleProject.get(); + if (project == null) { + return ImmutableList.of(); + } else { + ImmutableList classpathEntries = ImmutableList.copyOf(Stream.concat(project.getClasspath().stream().map(dep -> dep.getFile().toPath()), + project.getProjectDependencies().stream() + .map(d -> findPeer(root, d.getTargetProject().getName())) + .filter(o -> o.isPresent()) + .map(o -> o.get()) + .map(p -> p.getProjectDirectory().toPath().resolve(p.getOutputLocation().getPath())) + ).collect(Collectors.toList())); + return classpathEntries; + } } private Optional findPeer(EclipseProject root, String name) { @@ -108,26 +125,33 @@ public class GradleProjectClasspath extends JandexClasspath { } @Override - public Stream getClasspathResources() { - return gradleProject.getSourceDirectories().stream().map(sourceDirectory -> sourceDirectory.getDirectory()).flatMap(folder -> { - try { - return Files.walk(folder.toPath()) - .filter(path -> Files.isRegularFile(path)) - .map(path -> folder.toPath().relativize(path)) - .map(relativePath -> relativePath.toString()) - .filter(pathString -> !pathString.endsWith(".java") && !pathString.endsWith(".class")); - } catch (IOException e) { - return Stream.empty(); - } - }); + public ImmutableList getClasspathResources() { + EclipseProject project = gradleProject.get(); + if (project == null) { + return ImmutableList.of(); + } else { + return ImmutableList.copyOf(project.getSourceDirectories().stream().map(sourceDirectory -> sourceDirectory.getDirectory()).flatMap(folder -> { + try { + return Files.walk(folder.toPath()) + .filter(path -> Files.isRegularFile(path)) + .map(path -> folder.toPath().relativize(path)) + .map(relativePath -> relativePath.toString()) + .filter(pathString -> !pathString.endsWith(".java") && !pathString.endsWith(".class")); + } catch (IOException e) { + return Stream.empty(); + } + }).toArray(String[]::new)); + } } public Path getOutputFolder() { - return gradleProject.getProjectDirectory().toPath().resolve(gradleProject.getOutputLocation().getPath()); + EclipseProject project = gradleProject.get(); + return project == null ? null : project.getProjectDirectory().toPath().resolve(project.getOutputLocation().getPath()); } public String getName() { - return gradleProject.getName(); + EclipseProject project = gradleProject.get(); + return project == null ? null : project.getName(); } public boolean exists() { @@ -136,20 +160,23 @@ public class GradleProjectClasspath extends JandexClasspath { @Override protected IJavadocProvider createParserJavadocProvider(File classpathResource) { - if (classpathResource.isDirectory()) { - Optional classpathFolder = gradleProject.getSourceDirectories().stream() - .map(dir -> dir.getDirectory()) - .filter(dir -> classpathResource.toPath().startsWith(dir.toPath())) - .findFirst(); - if (classpathFolder.isPresent()) { - return new ParserJavadocProvider(type -> { - return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER - .sourceUrl(classpathFolder.get().toURI().toURL(), type); - }); + EclipseProject project = gradleProject.get(); + if (project != null) { + if (classpathResource.isDirectory()) { + Optional classpathFolder = project.getSourceDirectories().stream() + .map(dir -> dir.getDirectory()) + .filter(dir -> classpathResource.toPath().startsWith(dir.toPath())) + .findFirst(); + if (classpathFolder.isPresent()) { + return new ParserJavadocProvider(type -> { + return SourceUrlProviderFromSourceContainer.SOURCE_FOLDER_URL_SUPPLIER + .sourceUrl(classpathFolder.get().toURI().toURL(), type); + }); + } + } else { + } - } else { - } return null; } @@ -222,4 +249,13 @@ public class GradleProjectClasspath extends JandexClasspath { return new File(JandexIndex.getIndexFolder().toString(), jarFile.getName() + "-" + suffix + ".jdx"); } + @Override + public boolean equals(Object obj) { + if (obj instanceof GradleProjectClasspath) { + return super.equals(obj); + } + return false; + } + + } diff --git a/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java b/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java index 2068c29a1..4e207ac69 100644 --- a/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java +++ b/headless-services/commons/commons-gradle/src/test/java/org/springframework/ide/vscode/commons/gradle/GradleProjectTest.java @@ -13,21 +13,26 @@ package org.springframework.ide.vscode.commons.gradle; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; +import org.assertj.core.util.Files; import org.junit.Test; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener; import org.springframework.ide.vscode.commons.util.BasicFileObserver; +import com.google.common.collect.ImmutableList; + /** * Tests covering Gradle project data * @@ -36,15 +41,27 @@ import org.springframework.ide.vscode.commons.util.BasicFileObserver; */ public class GradleProjectTest { + private static void writeContent(File file, String content) throws IOException { + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.write(content); + } finally { + writer.close(); + } + } + private GradleJavaProject getGradleProject(String projectName) throws Exception { Path testProjectPath = Paths.get(GradleProjectTest.class.getResource("/" + projectName).toURI()); - return new GradleJavaProject(GradleCore.getDefault(), testProjectPath.toFile()); + GradleJavaProject gradleJavaProject = new GradleJavaProject(GradleCore.getDefault(), testProjectPath.toFile(), null); + gradleJavaProject.update(); + return gradleJavaProject; } @Test public void testEclipseGradleProject() throws Exception { GradleJavaProject project = getGradleProject("empty-gradle-project"); - Set calculatedClassPath = project.getClasspath().getClasspathEntries().collect(Collectors.toSet()); + ImmutableList calculatedClassPath = project.getClasspath().getClasspathEntries(); assertEquals(48, calculatedClassPath.size()); } @@ -57,7 +74,7 @@ public class GradleProjectTest { @Test public void gradleClasspathResource() throws Exception { GradleJavaProject project = getGradleProject("test-app-1"); - List resources = project.getClasspath().getClasspathResources().collect(Collectors.toList()); + List resources = project.getClasspath().getClasspathResources(); assertArrayEquals(new String[] {"test-resource-1.txt"}, resources.toArray(new String[resources.size()])); } @@ -65,38 +82,57 @@ public class GradleProjectTest { public void testGradleFileChanges() throws Exception { Path testProjectPath = Paths.get(GradleProjectTest.class.getResource("/empty-gradle-project").toURI()); File gradleFile = testProjectPath.resolve(GradleCore.GRADLE_BUILD_FILE).toFile(); - BasicFileObserver fileObserver = new BasicFileObserver(); - GradleProjectCache manager = new GradleProjectCache(fileObserver, GradleCore.getDefault()); - IJavaProject[] projectChanged = new IJavaProject[] { null }; - IJavaProject[] projectDeleted = new IJavaProject[] { null }; - manager.addListener(new Listener() { - @Override - public void created(IJavaProject project) {} - - @Override - public void changed(IJavaProject project) { - projectChanged[0] = project; - } - @Override - public void deleted(IJavaProject project) { - projectDeleted[0] = project; - } - }); - // Get the project from cache - GradleJavaProject cachedProject = manager.project(gradleFile); - assertNotNull(cachedProject); + String gradelFileContents = Files.contentOf(gradleFile, Charset.defaultCharset()); - fileObserver.notifyFileChanged(gradleFile.toURI().toString()); - assertEquals(cachedProject, projectChanged[0]); - - fileObserver.notifyFileDeleted(gradleFile.toURI().toString()); - assertEquals(cachedProject, projectDeleted[0]); + try { + BasicFileObserver fileObserver = new BasicFileObserver(); + GradleProjectCache manager = new GradleProjectCache(fileObserver, GradleCore.getDefault(), false, null); + IJavaProject[] projectChanged = new IJavaProject[] { null }; + IJavaProject[] projectDeleted = new IJavaProject[] { null }; + + // Get the project from cache + GradleJavaProject cachedProject = manager.project(gradleFile); + assertNotNull(cachedProject); + + manager.addListener(new Listener() { + @Override + public void created(IJavaProject project) {} + + @Override + public void changed(IJavaProject project) { + projectChanged[0] = project; + } + @Override + public void deleted(IJavaProject project) { + projectDeleted[0] = project; + } + }); + + ImmutableList calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(48, calculatedClassPath.size()); + + fileObserver.notifyFileChanged(gradleFile.toURI().toString()); + assertNull(projectChanged[0]); + + writeContent(gradleFile, Files.contentOf(testProjectPath.resolve("build.newgradle").toFile(), Charset.defaultCharset())); + fileObserver.notifyFileChanged(gradleFile.toURI().toString()); + assertNotNull(projectChanged[0]); + assertEquals(cachedProject, projectChanged[0]); + calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(49, calculatedClassPath.size()); + + + fileObserver.notifyFileDeleted(gradleFile.toURI().toString()); + assertEquals(cachedProject, projectDeleted[0]); + } finally { + writeContent(gradleFile, gradelFileContents); + } } @Test public void findGradleProjectWithStandardBuildFile() throws Exception { - GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault())); + GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault(), false, null)); File sourceFile = new File(GradleProjectTest.class.getResource("/test-app-1/src/main/java/Library.java").toURI()); Optional project = finder.find(sourceFile); assertTrue(project.isPresent()); @@ -107,7 +143,7 @@ public class GradleProjectTest { @Test public void findGradleProjectWithNonStandardBuildFile() throws Exception { - GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault())); + GradleProjectFinder finder = new GradleProjectFinder(new GradleProjectCache(new BasicFileObserver(), GradleCore.getDefault(), false, null)); File sourceFile = new File(GradleProjectTest.class.getResource("/test-app-2/src/main/java/Library.java").toURI()); Optional project = finder.find(sourceFile); assertTrue(project.isPresent()); diff --git a/headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle b/headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle new file mode 100644 index 000000000..e01b09e0d --- /dev/null +++ b/headless-services/commons/commons-gradle/src/test/resources/empty-gradle-project/build.newgradle @@ -0,0 +1,34 @@ +buildscript { + ext { + springBootVersion = '1.5.1.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' + +jar { + baseName = 'empty-boot-1.4.0-web-app' + version = '0.0.1-SNAPSHOT' +} + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + + +dependencies { + compile('org.springframework.boot:spring-boot-starter-actuator') + compile('org.springframework.boot:spring-boot-starter-web') + compile('org.springframework.boot:spring-boot-devtools') + testCompile('org.springframework.boot:spring-boot-starter-test') +} diff --git a/headless-services/commons/commons-java/pom.xml b/headless-services/commons/commons-java/pom.xml index 756288fb5..52f92d2ea 100644 --- a/headless-services/commons/commons-java/pom.xml +++ b/headless-services/commons/commons-java/pom.xml @@ -46,5 +46,10 @@ reactor-core ${reactor-version} + + org.json + json + 20160810 + \ No newline at end of file diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java index c90066026..63d200b0e 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jandex/JandexClasspath.java @@ -35,7 +35,7 @@ public abstract class JandexClasspath implements IClasspath { protected JandexIndex createIndex() { Stream classpathEntries = Stream.empty(); try { - classpathEntries = getClasspathEntries(); + classpathEntries = getClasspathEntries().stream(); } catch (Exception e) { Log.log(e); } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java new file mode 100644 index 000000000..8bb376491 --- /dev/null +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/AbstractJavaProject.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.java; + +import java.nio.file.Path; + +/** + * Abstract java project. Has a folder to store some project calculated data to speed up access + * + * @author Alex Boyko + * + */ +public abstract class AbstractJavaProject implements IJavaProject { + + final protected Path projectDataCache; + + public AbstractJavaProject(Path projectDataCache) { + this.projectDataCache = projectDataCache; + } + +} diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java new file mode 100644 index 000000000..4e2e4824e --- /dev/null +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/DelegatingCachedClasspath.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.java; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.ide.vscode.commons.util.Log; + +import com.google.common.base.Objects; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; + +import reactor.core.publisher.Flux; +import reactor.util.function.Tuple2; + +/** + * A wrapper around the classpath created from a Java project using the data in the project file (maven, gradle) + * The wrapper caches some of classpath data such as + *

  • Classpath entries
  • + *
  • Classpath resources
  • + *
  • Output folder
  • + *
  • Projects' name
  • + * + * The cached classpath data is written to ".sts4-cache/classpath-data.json" and loadedd from it when intance of this classpath is created + * + * Implementation is somewhat experimental at the moment... + * + * @author Alex Boyko + * + * @param a subclass of {@link IClasspath} the delegated to classpath created from current data + */ +public class DelegatingCachedClasspath implements IClasspath { + + public static final String CLASSPATH_DATA_CACHE_FILE = "classpath-data.json"; + + private static final String OUTPUT_FOLDER_PROPERTY = "outputFolder"; + private static final String CLASSPATH_RESOURCES_PROPERTY = "classpathResources"; + private static final String CLASSPATH_ENTRIES_PROPERTY = "classpathEntries"; + private static final String NAME_PROPERTY = "name"; + + protected static class ClasspathData { + + final public String name; + final public Set classpathEntries; + final public Set classpathResources; + final public Path outputFolder; + + public ClasspathData(String name, Set classpathEntries, Set classpathResources, Path outputFolder) { + this.name = name; + this.classpathEntries = classpathEntries; + this.classpathResources = classpathResources; + this.outputFolder = outputFolder; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ClasspathData) { + ClasspathData other = (ClasspathData) obj; + try { + return Objects.equal(name, other.name) + && Objects.equal(classpathEntries, other.classpathEntries) + && Objects.equal(classpathResources, other.classpathResources) + && Objects.equal(outputFolder, outputFolder); + } catch (Throwable t) { + Log.log(t); + } + } + return false; + } + + } + + private AtomicReference cachedData; + private Supplier delegateCreator; + private AtomicReference cachedDelegate; + + final private File cacheFile; + + public DelegatingCachedClasspath(Supplier delegateCreator, File cacheFile) { + super(); + this.cacheFile = cacheFile; + this.cachedDelegate = new AtomicReference<>(delegateCreator.get()); + this.cachedData = new AtomicReference<>(init()); + this.delegateCreator = delegateCreator; + if (!isCached()) { + update(); + } + } + + public T delegate() { + return cachedDelegate.get(); + } + + @Override + public String getName() { + return cachedData.get().name; + } + + @Override + public Path getOutputFolder() { + return cachedData.get().outputFolder; + } + + @Override + public ImmutableList getClasspathEntries() throws Exception { + return ImmutableList.copyOf(cachedData.get().classpathEntries); + } + + @Override + public ImmutableList getClasspathResources() { + return ImmutableList.copyOf(cachedData.get().classpathResources); + } + + public boolean isCached() { + return cacheFile != null && cacheFile.exists(); + } + + private synchronized ClasspathData loadCachedData() { + if (cacheFile != null && cacheFile.exists()) { + try { + JSONObject json = new JSONObject(new JSONTokener(new FileInputStream(cacheFile))); + String name = json.getString(NAME_PROPERTY); + JSONArray classpathEntriesJson = json.optJSONArray(CLASSPATH_ENTRIES_PROPERTY); + JSONArray classpathResourcesJson = json.optJSONArray(CLASSPATH_RESOURCES_PROPERTY); + String outputFolderStr = json.optString(OUTPUT_FOLDER_PROPERTY); + + return new ClasspathData( + name, + classpathEntriesJson == null ? Collections.emptySet() : classpathEntriesJson.toList().stream() + .filter(o -> o instanceof String) + .map(o -> (String) o) + .map(s -> new File(s).toPath()) + .collect(Collectors.toSet()), + classpathResourcesJson == null ? Collections.emptySet() : classpathResourcesJson.toList().stream() + .filter(o -> o instanceof String) + .map(o -> (String) o) + .collect(Collectors.toSet()), + outputFolderStr == null ? null : new File(outputFolderStr).toPath() + ); + } catch (Throwable e) { + Log.log(e); + } + } + return null; + } + + private ClasspathData init() { + ClasspathData data = loadCachedData(); + return data == null ? new ClasspathData(null, Collections.emptySet(), Collections.emptySet(), null) : data; + } + + private synchronized void persistCachedData(ClasspathData data) { + if (cacheFile != null && data != null) { + FileWriter writer = null; + try { + Files.createDirectories(cacheFile.getParentFile().toPath()); + JSONObject json = new JSONObject(); + json.put(NAME_PROPERTY, data.name); + json.put(CLASSPATH_ENTRIES_PROPERTY, data.classpathEntries.stream().map(e -> e.toString()).collect(Collectors.toList())); + json.put(CLASSPATH_RESOURCES_PROPERTY, data.classpathResources); + json.put(OUTPUT_FOLDER_PROPERTY, data.outputFolder); + writer = new FileWriter(cacheFile); + json.write(writer); + } catch (IOException e) { + Log.log(e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + Log.log(e); + } + } + } + } + } + + public boolean update() { + final ClasspathData newData = createClasspathData(); + if (!Objects.equal(cachedData.get(), newData)) { + cachedData.set(newData); + persistCachedData(newData); + return true; + } + return false; + } + + @Override + public boolean exists() { + return cachedDelegate.get().exists(); + } + + @Override + public IType findType(String fqName) { + return cachedDelegate.get().findType(fqName); + } + + @Override + public Flux> fuzzySearchTypes(String searchTerm, Predicate typeFilter) { + return cachedDelegate.get().fuzzySearchTypes(searchTerm, typeFilter); + } + + @Override + public Flux> fuzzySearchPackages(String searchTerm) { + return cachedDelegate.get().fuzzySearchPackages(searchTerm); + } + + @Override + public Flux allSubtypesOf(IType type) { + return cachedDelegate.get().allSubtypesOf(type); + } + + protected ClasspathData createClasspathData() { + T newDelegate = delegateCreator.get(); + cachedDelegate.set(newDelegate); + try { + LinkedHashSet classpathEntries = new LinkedHashSet<>(newDelegate.getClasspathEntries()); + return new ClasspathData(newDelegate.getName(), classpathEntries, new LinkedHashSet<>(newDelegate.getClasspathResources()), newDelegate.getOutputFolder()); + } catch (Exception e) { + Log.log(e); + return new ClasspathData(newDelegate.getName(), Collections.emptySet(), new LinkedHashSet<>(newDelegate.getClasspathResources()), newDelegate.getOutputFolder()); + } + } + +} diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java index a30663512..79722f8c3 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspath.java @@ -12,7 +12,8 @@ package org.springframework.ide.vscode.commons.java; import java.nio.file.Path; import java.util.function.Predicate; -import java.util.stream.Stream; + +import com.google.common.collect.ImmutableList; import reactor.core.publisher.Flux; import reactor.util.function.Tuple2; @@ -46,12 +47,12 @@ public interface IClasspath { * @return collection of classpath entries in a form file/folder paths * @throws Exception */ - Stream getClasspathEntries() throws Exception; + ImmutableList getClasspathEntries() throws Exception; /** * Classpath resources paths relative to the source folder path * @return classpath resource relative paths */ - Stream getClasspathResources(); - + ImmutableList getClasspathResources(); + } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java index 1b9631e3e..06c2e3b31 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IJavaProject.java @@ -14,6 +14,8 @@ import org.springframework.ide.vscode.commons.javadoc.IJavadoc; public interface IJavaProject extends IJavaElement { + final static String PROJECT_CACHE_FOLDER = ".sts4-cache"; + IClasspath getClasspath(); @Override diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java index 2512338ff..abd2f3da5 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/languageserver/java/AbstractFileToProjectCache.java @@ -11,14 +11,16 @@ package org.springframework.ide.vscode.commons.languageserver.java; import java.io.File; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.FileObserver; /** - * Cache fo java projects. The key for the cache is a "project" specific file + * Cache for java projects. The key for the cache is a "project" specific file * * @author Alex Boyko * @@ -28,19 +30,26 @@ public abstract class AbstractFileToProjectCache

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

    extends }); } - abstract protected void update(P project); + final protected void performUpdate(P project, boolean async) { + if (async) { + CompletableFuture.supplyAsync(() -> update(project)).thenAccept((changed) -> { + if (changed || alwaysFireEventOnFileChanged) { + notifyProjectChanged(project); + } + }); + } else { + if (update(project) || alwaysFireEventOnFileChanged) { + notifyProjectChanged(project); + } + } + } + + abstract protected boolean update(P project); } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java index 8e0728bb4..e2ace4ecc 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenJavaProject.java @@ -11,8 +11,10 @@ package org.springframework.ide.vscode.commons.maven.java; import java.io.File; +import java.nio.file.Path; -import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.AbstractJavaProject; +import org.springframework.ide.vscode.commons.java.DelegatingCachedClasspath; import org.springframework.ide.vscode.commons.maven.MavenCore; /** @@ -21,21 +23,29 @@ import org.springframework.ide.vscode.commons.maven.MavenCore; * @author Alex Boyko * */ -public class MavenJavaProject implements IJavaProject { - - private MavenProjectClasspath classpath; - - public MavenJavaProject(MavenCore maven, File pom) { - this.classpath = new MavenProjectClasspath(maven, pom); +public class MavenJavaProject extends AbstractJavaProject { + + private DelegatingCachedClasspath classpath; + + public MavenJavaProject(MavenCore maven, File pom, Path projectDataCache) { + super(projectDataCache); + this.classpath = new DelegatingCachedClasspath<>( + () -> new MavenProjectClasspath(maven, pom), + projectDataCache == null ? null : projectDataCache.resolve(DelegatingCachedClasspath.CLASSPATH_DATA_CACHE_FILE).toFile() + ); } - + + public MavenJavaProject(MavenCore maven, File pom) { + this(maven, pom, null); + } + @Override - public MavenProjectClasspath getClasspath() { + public DelegatingCachedClasspath getClasspath() { return classpath; } - void update(MavenCore maven) { - this.classpath = new MavenProjectClasspath(maven, classpath.getPomFile()); + boolean update() { + return classpath.update(); } - + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java index bae929e28..227964cb4 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/MavenProjectCache.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.commons.maven.java; import java.io.File; +import java.nio.file.Path; import org.springframework.ide.vscode.commons.languageserver.java.AbstractFileToProjectCache; import org.springframework.ide.vscode.commons.maven.MavenCore; @@ -25,19 +26,24 @@ public class MavenProjectCache extends AbstractFileToProjectCache getClasspathEntries() throws Exception { + public ImmutableList getClasspathEntries() throws Exception { // return Stream.concat(maven.resolveDependencies(project, null).stream().map(artifact -> { // return artifact.getFile().toPath(); // }), projectResolvedOutput()); - return Stream.concat(projectDependencies().stream().map(a -> a.getFile().toPath()), projectOutput().stream().map(f -> f.toPath())); + ImmutableList classpathEntries = ImmutableList.copyOf(Stream.concat(projectDependencies().stream().map(a -> a.getFile().toPath()), + projectOutput().stream().map(f -> f.toPath())).collect(Collectors.toList())); + return classpathEntries; } private Set projectDependencies() { @@ -123,12 +131,12 @@ public class MavenProjectClasspath extends JandexClasspath { } @Override - public Stream getClasspathResources() { + public ImmutableList getClasspathResources() { MavenProject project = projectSupplier.get(); if (project == null) { - return Stream.empty(); + return ImmutableList.of(); } - return project.getBuild().getResources().stream().flatMap(resource -> { + return ImmutableList.copyOf(project.getBuild().getResources().stream().filter(resource -> new File(resource.getDirectory()).exists()).flatMap(resource -> { DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(resource.getDirectory()); if (resource.getIncludes() != null && !resource.getIncludes().isEmpty()) { @@ -140,7 +148,7 @@ public class MavenProjectClasspath extends JandexClasspath { scanner.setCaseSensitive(false); scanner.scan(); return Arrays.stream(scanner.getIncludedFiles()); - }); + }).toArray(String[]::new)); } /* @@ -253,4 +261,20 @@ public class MavenProjectClasspath extends JandexClasspath { } } + @Override + public boolean equals(Object obj) { + if (obj instanceof MavenProjectClasspath) { + MavenProjectClasspath other = (MavenProjectClasspath) obj; + try { + if (pom.equals(other.pom) + && projectSupplier.get().equals(other.projectSupplier.get())) { + return super.equals(obj); + } + } catch (Throwable t) { + Log.log(t); + } + } + return false; + } + } diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java index 2b2157ebe..ea9f8da65 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/FileClasspath.java @@ -13,12 +13,15 @@ package org.springframework.ide.vscode.commons.maven.java.classpathfile; import java.nio.file.Files; import java.nio.file.Path; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IType; import org.springframework.ide.vscode.commons.maven.MavenCore; +import com.google.common.collect.ImmutableList; + import reactor.core.publisher.Flux; import reactor.util.function.Tuple2; @@ -37,15 +40,15 @@ public class FileClasspath implements IClasspath { } @Override - public Stream getClasspathEntries() throws Exception { - return Stream.concat(MavenCore.readClassPathFile(classpathFilePath), + public ImmutableList getClasspathEntries() throws Exception { + return ImmutableList.copyOf(Stream.concat(MavenCore.readClassPathFile(classpathFilePath), Stream.of(classpathFilePath.getParent().resolve("target/classes"), - classpathFilePath.getParent().resolve("target/test-classes"))); + classpathFilePath.getParent().resolve("target/test-classes"))).collect(Collectors.toList())); } @Override - public Stream getClasspathResources() { - return Stream.empty(); + public ImmutableList getClasspathResources() { + return ImmutableList.of(); } @Override diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java index 7e764ce8c..a48565f6f 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFile.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.commons.maven.java.classpathfile; import java.io.File; import java.nio.file.Paths; +import java.util.concurrent.CompletableFuture; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -67,8 +68,18 @@ public class JavaProjectWithClasspathFile implements IJavaProject { return true; } - void update() { - this.classpath = new FileClasspath(Paths.get(cpFile.toURI())); + CompletableFuture update() { + return CompletableFuture.supplyAsync(() -> doUpdate()); + } + + private synchronized boolean doUpdate() { + FileClasspath newClasspath = new FileClasspath(Paths.get(cpFile.toURI())); + if (newClasspath.equals(classpath)) { + return false; + } else { + this.classpath = newClasspath; + return true; + } } } \ No newline at end of file diff --git a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java index 95fc1897e..6e459ffe9 100644 --- a/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java +++ b/headless-services/commons/commons-maven/src/main/java/org/springframework/ide/vscode/commons/maven/java/classpathfile/JavaProjectWithClasspathFileCache.java @@ -18,12 +18,13 @@ import org.springframework.ide.vscode.commons.util.FileObserver; public class JavaProjectWithClasspathFileCache extends AbstractFileToProjectCache { public JavaProjectWithClasspathFileCache(FileObserver fileObserver) { - super(fileObserver); + super(fileObserver, false, null); } @Override - protected void update(JavaProjectWithClasspathFile project) { - project.update(); + protected boolean update(JavaProjectWithClasspathFile project) { + project.update(); + return true; } @Override diff --git a/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java b/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java index b30302b1a..4a419fe19 100644 --- a/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java +++ b/headless-services/commons/commons-maven/src/test/java/org/springframework/ide/vscode/commons/maven/MavenProjectManagerTest.java @@ -12,11 +12,16 @@ package org.springframework.ide.vscode.commons.maven; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; +import org.assertj.core.util.Files; import org.junit.Test; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver.Listener; @@ -24,6 +29,8 @@ import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; import org.springframework.ide.vscode.commons.maven.java.MavenProjectCache; import org.springframework.ide.vscode.commons.util.BasicFileObserver; +import com.google.common.collect.ImmutableList; + /** * Tests for {@link MavenProjectCache} * @@ -31,38 +38,66 @@ import org.springframework.ide.vscode.commons.util.BasicFileObserver; * */ public class MavenProjectManagerTest { - + + private static void writeContent(File file, String content) throws IOException { + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.write(content); + } finally { + writer.close(); + } + } + @Test public void testPomFileChanges() throws Exception { Path testProjectPath = Paths.get(DependencyTreeTest.class.getResource("/empty-boot-project-with-classpath-file").toURI()); File pomFile = testProjectPath.resolve(MavenCore.POM_XML).toFile(); - BasicFileObserver fileObserver = new BasicFileObserver(); - MavenProjectCache cache = new MavenProjectCache(fileObserver, MavenCore.getDefault()); - IJavaProject[] projectChanged = new IJavaProject[] { null }; - IJavaProject[] projectDeleted = new IJavaProject[] { null }; - cache.addListener(new Listener() { - @Override - public void created(IJavaProject project) {} + + String pomFileContents = Files.contentOf(pomFile, Charset.defaultCharset()); - @Override - public void changed(IJavaProject project) { - projectChanged[0] = project; - } - @Override - public void deleted(IJavaProject project) { - projectDeleted[0] = project; - } - }); - - // Get the project from cache - MavenJavaProject cachedProject = cache.project(pomFile); - assertNotNull(cachedProject); - - fileObserver.notifyFileChanged(pomFile.toURI().toString()); - assertEquals(cachedProject, projectChanged[0]); - - fileObserver.notifyFileDeleted(pomFile.toURI().toString()); - assertEquals(cachedProject, projectDeleted[0]); + try { + BasicFileObserver fileObserver = new BasicFileObserver(); + MavenProjectCache cache = new MavenProjectCache(fileObserver, MavenCore.getDefault(), false, null); + IJavaProject[] projectChanged = new IJavaProject[] { null }; + IJavaProject[] projectDeleted = new IJavaProject[] { null }; + cache.addListener(new Listener() { + @Override + public void created(IJavaProject project) {} + + @Override + public void changed(IJavaProject project) { + projectChanged[0] = project; + } + @Override + public void deleted(IJavaProject project) { + projectDeleted[0] = project; + } + }); + + // Get the project from cache + MavenJavaProject cachedProject = cache.project(pomFile); + assertNotNull(cachedProject); + + ImmutableList calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(48, calculatedClassPath.size()); + + fileObserver.notifyFileChanged(pomFile.toURI().toString()); + assertNull(projectChanged[0]); + + writeContent(pomFile, Files.contentOf(testProjectPath.resolve("pom.newxml").toFile(), Charset.defaultCharset())); + fileObserver.notifyFileChanged(pomFile.toURI().toString()); + assertNotNull(projectChanged[0]); + assertEquals(cachedProject, projectChanged[0]); + calculatedClassPath = cachedProject.getClasspath().getClasspathEntries(); + assertEquals(49, calculatedClassPath.size()); + + fileObserver.notifyFileDeleted(pomFile.toURI().toString()); + assertEquals(cachedProject, projectDeleted[0]); + } finally { + //restore original content + writeContent(pomFile, pomFileContents); + } } diff --git a/headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml b/headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml new file mode 100644 index 000000000..86392defd --- /dev/null +++ b/headless-services/commons/commons-maven/src/test/resources/empty-boot-project-with-classpath-file/pom.newxml @@ -0,0 +1,72 @@ + + + 4.0.0 + + com.example + empty-boot-1.4.0-web-app + 0.0.1-SNAPSHOT + jar + + empty-boot-project-with-classpath-file + Empty Boot project generating classpath.txt file + + + org.springframework.boot + spring-boot-starter-parent + 1.4.1.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-devtools + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + build-classpath + generate-sources + + build-classpath + + + + + classpath.txt + + + + + + + From c0013de8404aa93f0694d178807e6c701e18b199 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 16 Nov 2017 09:07:48 +0100 Subject: [PATCH 4/8] updated windows ico file --- .../sts4.ico | Bin 46752 -> 292878 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/eclipse-distribution/org.springframework.boot.ide.branding/sts4.ico b/eclipse-distribution/org.springframework.boot.ide.branding/sts4.ico index 2c0cad1ce4d6813f5af21b82abc161299aa8edef..4985ead72e0715ea837ba27b9db2973f4bbf971d 100644 GIT binary patch literal 292878 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 Ye!+ae*PfA<(F0yQ)p}C?$SmZ4002w<+yDRo From fc0f1494635562ab5a68f958c65e4cd7ea4437fc Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 16 Nov 2017 09:08:20 +0100 Subject: [PATCH 5/8] added script to generate windows icon --- .../makeWinIcon.sh | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 eclipse-distribution/org.springframework.boot.ide.branding/makeWinIcon.sh 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 From dbc8995c86d7fd70bed1a3e218342455ea4f8600 Mon Sep 17 00:00:00 2001 From: nsingh Date: Thu, 16 Nov 2017 10:13:21 -0800 Subject: [PATCH 6/8] PT 152579435 - Add hint message in quick in-place dialogue --- .../ls/eclipse/gotosymbol/dialogs/GotoSymbolDialog.java | 2 ++ .../ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java | 5 +++++ 2 files changed, 7 insertions(+) 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..64b18e158 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) { @@ -149,6 +150,10 @@ public class GotoSymbolDialogModel { public LiveVariable getSearchBox() { return searchBox; } + + public String getSearchBoxHintMessage() { + return SEARCH_BOX_HINT_MESSAGE; + } public LiveExpression getStatus() { return status; From 943351d8606bb37420c38af415da0b867675e183 Mon Sep 17 00:00:00 2001 From: nsingh Date: Thu, 16 Nov 2017 10:45:05 -0800 Subject: [PATCH 7/8] PT 152579372 - Make query search case insensitive --- .../eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 64b18e158..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 @@ -98,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; @@ -114,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(); } }; From 613b79cb359e0e5441c382f011cd1ddaccaca6f3 Mon Sep 17 00:00:00 2001 From: nsingh Date: Thu, 16 Nov 2017 11:50:30 -0800 Subject: [PATCH 8/8] Added case insensitive search for in workspace symbol search --- .../vscode/boot/java/utils/SpringIndexer.java | 17 ++--------------- .../ide/vscode/commons/util/StringUtil.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 15 deletions(-) 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 811d93b2c..1553f4751 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 @@ -54,6 +54,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; @@ -241,7 +242,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(); @@ -250,20 +251,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()) 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 343a2c9ca..a37cfdc4f 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 @@ -217,4 +217,22 @@ public class StringUtil { } return Optional.empty(); } + + 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; + } }