From c102efa83890115ee4010fa3ca07168f333dc9e7 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 7 Dec 2017 21:33:41 +0100 Subject: [PATCH] enable live hovers for inner classes --- .../ComponentInjectionsHoverProvider.java | 7 ++ .../boot/java/livehover/LiveHoverUtils.java | 12 +-- .../RequestMappingHoverProvider.java | 35 +++++--- .../BeanInjectedIntoHoverProviderTest.java | 60 +++++++++++++ .../ComponentInjectionsHoverProviderTest.java | 87 ++++++++++++++++++- .../test/RequestMappingLiveHoverTest.java | 43 +++++++-- .../java/example/InnerClassController.java | 28 ++++++ .../boot/app/cli/livebean/LiveBean.java | 15 +++- 8 files changed, 258 insertions(+), 29 deletions(-) create mode 100644 headless-services/boot-java-language-server/src/test/resources/test-projects/test-request-mapping-live-hover/src/main/java/example/InnerClassController.java diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java index 531ab8251..e9a3ed9d7 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -92,6 +92,13 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP return ASTUtils.getAttribute(annotation, "value").flatMap(ASTUtils::getFirstString) .orElseGet(() -> { String typeName = beanType.getName(); + + ITypeBinding declaringClass = beanType.getDeclaringClass(); + while (declaringClass != null) { + typeName = declaringClass.getName() + "." + typeName; + declaringClass = declaringClass.getDeclaringClass(); + } + if (StringUtil.hasText(typeName)) { return Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java index 7a681a967..72b27adc2 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java @@ -23,9 +23,9 @@ public class LiveHoverUtils { public static String showBean(LiveBean bean) { StringBuilder buf = new StringBuilder("Bean [id: " + bean.getId()); - String type = bean.getType(); - if (type!=null) { - buf.append(", type: `"+type+"`"); + String type = bean.getType(true); + if (type != null) { + buf.append(", type: `" + type + "`"); } buf.append(']'); return buf.toString(); @@ -35,10 +35,10 @@ public class LiveHoverUtils { String newline = " \n"+indentStr; //Note: the double space before newline makes markdown see it as a real line break StringBuilder buf = new StringBuilder("Bean: "); buf.append(bean.getId()); - String type = bean.getType(); - if (type!=null) { + String type = bean.getType(true); + if (type != null) { buf.append(newline); - buf.append("Type: `"+type+"`"); + buf.append("Type: `" + type + "`"); } String resource = bean.getResource(); if (StringUtil.hasText(resource)) { diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java index 372404e6b..1726a4cab 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java @@ -119,19 +119,28 @@ public class RequestMappingHoverProvider implements HoverProvider { private boolean methodMatchesAnnotation(Annotation annotation, RequestMapping rm) { String rqClassName = rm.getFullyQualifiedClassName(); - ASTNode parent = annotation.getParent(); - if (parent instanceof MethodDeclaration) { - MethodDeclaration methodDec = (MethodDeclaration) parent; - IMethodBinding binding = methodDec.resolveBinding(); - return binding.getDeclaringClass().getQualifiedName().equals(rqClassName) - && binding.getName().equals(rm.getMethodName()) - && Arrays.equals(Arrays.stream(binding.getParameterTypes()) - .map(t -> t.getTypeDeclaration().getQualifiedName()) - .toArray(String[]::new), - rm.getMethodParameters()); -// } else if (parent instanceof TypeDeclaration) { -// TypeDeclaration typeDec = (TypeDeclaration) parent; -// return typeDec.resolveBinding().getQualifiedName().equals(rqClassName); + if (rqClassName != null) { + int chop = rqClassName.indexOf("$$EnhancerBySpringCGLIB$$"); + if (chop >= 0) { + rqClassName = rqClassName.substring(0, chop); + } + + rqClassName = rqClassName.replace('$', '.'); + + ASTNode parent = annotation.getParent(); + if (parent instanceof MethodDeclaration) { + MethodDeclaration methodDec = (MethodDeclaration) parent; + IMethodBinding binding = methodDec.resolveBinding(); + return binding.getDeclaringClass().getQualifiedName().equals(rqClassName) + && binding.getName().equals(rm.getMethodName()) + && Arrays.equals(Arrays.stream(binding.getParameterTypes()) + .map(t -> t.getTypeDeclaration().getQualifiedName()) + .toArray(String[]::new), + rm.getMethodParameters()); + // } else if (parent instanceof TypeDeclaration) { + // TypeDeclaration typeDec = (TypeDeclaration) parent; + // return typeDec.resolveBinding().getQualifiedName().equals(rqClassName); + } } return false; } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java index d5cc4b757..e4b736a1f 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java @@ -219,6 +219,66 @@ public class BeanInjectedIntoHoverProviderTest { ); } + @Test + public void beanFromInnerClassWithOneInjection() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("hello.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("hello.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("irrelevantBean") + .type("com.example.IrrelevantBean") + .dependencies("myController") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "public class OuterClass {\n" + + " \n" + + " @Configuration\n" + + " public static class InnerClass {\n" + + " \n" + + " @Bean(\"fooImplementation\")\n" + + " Foo someFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + " }\n" + + "}" + ); + editor.assertHighlights("@Bean"); + editor.assertTrimmedHover("@Bean", + "**Injection report for Bean [id: fooImplementation]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `hello.FooImplementation`] injected into:\n" + + "\n" + + "- Bean: myController \n" + + " Type: `hello.MyController`\n" + ); + } + @Test public void beanWithFileResource() throws Exception { Path of = harness.getOutputFolder(); diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java index 04367e536..b64f9e6e7 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java @@ -585,7 +585,8 @@ public class ComponentInjectionsHoverProviderTest { ); } - @Test public void bug_153072942_SpringBootApplication__withCGLib_is_a_Component() throws Exception { + @Test + public void bug_153072942_SpringBootApplication__withCGLib_is_a_Component() throws Exception { LiveBeansModel beans = LiveBeansModel.builder() .add(LiveBean.builder() .id("demoApplication") @@ -623,4 +624,88 @@ public class ComponentInjectionsHoverProviderTest { ); } + @Test + public void componentFromInnerClass() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("demoApplication.InnerClass") + .type("com.example.DemoApplication$InnerClass") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.boot.SpringApplication;\n" + + "import org.springframework.boot.autoconfigure.SpringBootApplication;\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.web.client.RestTemplate;\n" + + "\n" + + "public class DemoApplication {\n" + + " \n" + + " @SpringBootApplication\n" + + " public static class InnerClass {\n" + + " \n" + + " public static void main(String[] args) {\n" + + " SpringApplication.run(DemoApplication.InnerClass.class, args);\n" + + " }\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@SpringBootApplication"); + editor.assertHoverContains("@SpringBootApplication", + "**Injection report for Bean [id: demoApplication.InnerClass, type: `com.example.DemoApplication.InnerClass`]**" + ); + } + + @Test + public void componentFromInnerInnerClass() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("demoApplication.InnerClass.InnerInnerClass") + .type("com.example.DemoApplication$InnerClass$InnerInnerClass") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.boot.SpringApplication;\n" + + "import org.springframework.boot.autoconfigure.SpringBootApplication;\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.web.client.RestTemplate;\n" + + "\n" + + "public class DemoApplication {\n" + + " \n" + + " public static class InnerClass {\n" + + " \n" + + " @SpringBootApplication\n" + + " public static class InnerInnerClass {\n" + + " \n" + + " public static void main(String[] args) {\n" + + " SpringApplication.run(DemoApplication.InnerClass.InnerInnerClass.class, args);\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@SpringBootApplication"); + editor.assertHoverContains("@SpringBootApplication", + "**Injection report for Bean [id: demoApplication.InnerClass.InnerInnerClass, type: `com.example.DemoApplication.InnerClass.InnerInnerClass`]**" + ); + } } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java index 602c2ab5b..6260b002c 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java @@ -64,6 +64,7 @@ public class RequestMappingLiveHoverTest { harness.intialize(directory); Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); + editor.assertHighlights("@RequestMapping(method=RequestMethod.GET)"); editor.assertHoverContains("@RequestMapping(method=RequestMethod.GET)", "[http://cfapps.io:1111/hello-world](http://cfapps.io:1111/hello-world)\n" + "\n" + "Process [PID=22022, name=`test-request-mapping-live-hover`]"); @@ -94,6 +95,8 @@ public class RequestMappingLiveHoverTest { harness.intialize(directory); Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); + editor.assertHighlights("@RequestMapping(\"/hello\")", "@RequestMapping(\"/goodbye\")"); + editor.assertHoverContains("@RequestMapping(\"/hello\")", "[http://cfapps.io:999/hello](http://cfapps.io:999/hello)\n" + "\n" + "Process [PID=76543, name=`test-request-mapping-live-hover`]"); @@ -214,12 +217,8 @@ public class RequestMappingLiveHoverTest { docUri); editor.assertNoHover("@DeleteMapping(\"/greetings\")"); - } - - - @Test public void testNoHoverHintMethod() throws Exception { @@ -261,7 +260,6 @@ public class RequestMappingLiveHoverTest { docUri); editor.assertNoHover("@PutMapping(\"/greetings\")"); - } @Test @@ -756,4 +754,39 @@ public class RequestMappingLiveHoverTest { } + @Test + public void testMappingHoversInInnerClasses() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/InnerClassController.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("1111") + .processId("22022") + .host("cfapps.io") + .processName("test-request-mapping-live-hover") + // Ugly, but this is real JSON copied from a real live running app. We want the + // mock app to return realistic results if possible + .requestMappings( + "{\"/webjars/**\":{\"bean\":\"resourceHandlerMapping\"},\"/**\":{\"bean\":\"resourceHandlerMapping\"},\"/**/favicon.ico\":{\"bean\":\"faviconHandlerMapping\"},\"{[/hello-world],methods=[GET]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public example.Greeting example.HelloWorldController.sayHello(java.lang.String)\"},\"{[/inner-inner-class]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.InnerClassController$InnerController$InnerInnerController.saySomethingSuperInnerClass()\"},\"{[/inner-class]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.InnerClassController$InnerController.saySomething()\"},\"{[/person/{name}],methods=[GET]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.getMapping(java.lang.String)\"},\"{[/delete/{id}],methods=[DELETE]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.removeMe(int)\"},\"{[/goodbye]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.goodbye()\"},\"{[/hello]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.hello()\"},\"{[/postHello],methods=[POST]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.postMethod(java.lang.String)\"},\"{[/put/{id}],methods=[PUT]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.putMethod(int,java.lang.String)\"},\"{[/error]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)\"},\"{[/error],produces=[text/html]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)\"}}") + .build(); + + harness.intialize(directory); + + Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); + editor.assertHighlights("@RequestMapping(\"/inner-class\")", "@RequestMapping(\"/inner-inner-class\")"); + + editor.assertHoverContains("@RequestMapping(\"/inner-class\")", "[http://cfapps.io:1111/inner-class](http://cfapps.io:1111/inner-class)\n" + + "\n" + + "Process [PID=22022, name=`test-request-mapping-live-hover`]"); + + editor.assertHoverContains("@RequestMapping(\"/inner-inner-class\")", "[http://cfapps.io:1111/inner-inner-class](http://cfapps.io:1111/inner-inner-class)\n" + + "\n" + + "Process [PID=22022, name=`test-request-mapping-live-hover`]"); + } + } diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-request-mapping-live-hover/src/main/java/example/InnerClassController.java b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-request-mapping-live-hover/src/main/java/example/InnerClassController.java new file mode 100644 index 000000000..5401c345a --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-request-mapping-live-hover/src/main/java/example/InnerClassController.java @@ -0,0 +1,28 @@ +package example; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +public class InnerClassController { + + @RestController + public static class InnerController { + + @RequestMapping("/inner-class") + public String saySomething() { + return "Hello from Inner-Class"; + } + + @RestController + public static class InnerInnerController { + + @RequestMapping("/inner-inner-class") + public String saySomethingSuperInnerClass() { + return "Hello from Inner-Inner-Class"; + } + + } + + } + +} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java index d006a8497..e21badb98 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java @@ -95,12 +95,19 @@ public class LiveBean { public String getType(boolean stripCGLib) { String type = this.type; - if (stripCGLib) { - int chop = type.indexOf("$$EnhancerBySpringCGLIB$$"); - if (chop>=0) { - type = type.substring(0, chop); + + if (type != null) { + if (stripCGLib) { + int chop = type.indexOf("$$EnhancerBySpringCGLIB$$"); + if (chop >= 0) { + type = type.substring(0, chop); + } } + + // convert inner classes from $ to . notation + type = type.replace('$', '.'); } + return type; }