GH-758: restrict request mapping snippet completions to classes that are annotated as a spring controller
This commit is contained in:
@@ -10,14 +10,20 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.boot.app;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.core.dom.ASTNode;
|
||||
import org.eclipse.jdt.core.dom.IAnnotationBinding;
|
||||
import org.eclipse.jdt.core.dom.ITypeBinding;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.lsp4j.CompletionItemKind;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.ide.vscode.boot.java.Annotations;
|
||||
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
|
||||
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
|
||||
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
|
||||
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
|
||||
@@ -25,6 +31,7 @@ import org.springframework.ide.vscode.boot.java.scope.ScopeCompletionProcessor;
|
||||
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippet;
|
||||
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext;
|
||||
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetManager;
|
||||
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
|
||||
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
|
||||
import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor;
|
||||
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
|
||||
@@ -46,8 +53,13 @@ public class BootJavaCompletionEngineConfigurer {
|
||||
// STS3 bundle. Therefore do not include templates if client is Eclipse
|
||||
// TODO: REMOVE this check once STS3 is no longer supported
|
||||
if (LspClient.currentClient() != LspClient.Client.ECLIPSE) {
|
||||
|
||||
JavaSnippetContext webControllerContext = new CompositeJavaSnippetContext(
|
||||
JavaSnippetContext.AT_ROOT_LEVEL,
|
||||
new AnnotatedTypeDeclarationContext(Annotations.CONTROLLER));
|
||||
|
||||
snippetManager.add(
|
||||
new JavaSnippet("@RequestMapping(..) {..}", JavaSnippetContext.AT_ROOT_LEVEL, CompletionItemKind.Method,
|
||||
new JavaSnippet("@RequestMapping(..) {..}", webControllerContext, CompletionItemKind.Method,
|
||||
ImmutableList.of("org.springframework.web.bind.annotation.RequestMapping",
|
||||
"org.springframework.web.bind.annotation.RequestMethod",
|
||||
"org.springframework.web.bind.annotation.RequestParam"),
|
||||
@@ -56,7 +68,7 @@ public class BootJavaCompletionEngineConfigurer {
|
||||
+ " return new ${SomeData}(${cursor});\n" + "}\n",
|
||||
"RequestMapping"));
|
||||
snippetManager.add(
|
||||
new JavaSnippet("@GetMapping(..) {..}", JavaSnippetContext.AT_ROOT_LEVEL, CompletionItemKind.Method,
|
||||
new JavaSnippet("@GetMapping(..) {..}", webControllerContext, CompletionItemKind.Method,
|
||||
ImmutableList.of("org.springframework.web.bind.annotation.GetMapping",
|
||||
"org.springframework.web.bind.annotation.RequestParam"),
|
||||
"@GetMapping(\"${path}\")\n"
|
||||
@@ -64,7 +76,7 @@ public class BootJavaCompletionEngineConfigurer {
|
||||
+ " return new ${SomeData}(${cursor});\n" + "}\n",
|
||||
"GetMapping"));
|
||||
snippetManager.add(
|
||||
new JavaSnippet("@PostMapping(..) {..}", JavaSnippetContext.AT_ROOT_LEVEL, CompletionItemKind.Method,
|
||||
new JavaSnippet("@PostMapping(..) {..}", webControllerContext, CompletionItemKind.Method,
|
||||
ImmutableList.of("org.springframework.web.bind.annotation.PostMapping",
|
||||
"org.springframework.web.bind.annotation.RequestBody"),
|
||||
"@PostMapping(\"${path}\")\n"
|
||||
@@ -72,7 +84,7 @@ public class BootJavaCompletionEngineConfigurer {
|
||||
+ " //TODO: process POST request\n" + " ${cursor}\n" + " return ${entity};\n" + "}\n",
|
||||
"PostMapping"));
|
||||
snippetManager.add(
|
||||
new JavaSnippet("@PutMapping(..) {..}", JavaSnippetContext.AT_ROOT_LEVEL, CompletionItemKind.Method,
|
||||
new JavaSnippet("@PutMapping(..) {..}", webControllerContext, CompletionItemKind.Method,
|
||||
ImmutableList.of("org.springframework.web.bind.annotation.PutMapping",
|
||||
"org.springframework.web.bind.annotation.RequestBody",
|
||||
"org.springframework.web.bind.annotation.PathVariable"),
|
||||
@@ -102,4 +114,59 @@ public class BootJavaCompletionEngineConfigurer {
|
||||
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if the type declaration that belongs to the node is annotated with the required annotation
|
||||
*/
|
||||
private class AnnotatedTypeDeclarationContext implements JavaSnippetContext {
|
||||
|
||||
private final String requiredAnnotation;
|
||||
|
||||
public AnnotatedTypeDeclarationContext(String requiredAnnotation) {
|
||||
this.requiredAnnotation = requiredAnnotation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appliesTo(ASTNode node) {
|
||||
TypeDeclaration type = ASTUtils.findDeclaringType(node);
|
||||
if (type != null) {
|
||||
ITypeBinding binding = type.resolveBinding();
|
||||
if (binding != null) {
|
||||
IAnnotationBinding[] annotations = binding.getAnnotations();
|
||||
if (annotations != null) {
|
||||
for (int i = 0; i < annotations.length; i++) {
|
||||
ITypeBinding annotationType = annotations[i].getAnnotationType();
|
||||
if (AnnotationHierarchies.isMetaAnnotation(annotationType, (name) -> requiredAnnotation.equals(name))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* composite implementation for snippet contexts that allows you to combine multiple context checks
|
||||
* The check only passes if ALL individual context checks are successful
|
||||
*/
|
||||
private class CompositeJavaSnippetContext implements JavaSnippetContext {
|
||||
|
||||
private final JavaSnippetContext[] contexts;
|
||||
|
||||
public CompositeJavaSnippetContext(JavaSnippetContext... contexts) {
|
||||
this.contexts = contexts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean appliesTo(ASTNode node) {
|
||||
for (JavaSnippetContext context : contexts) {
|
||||
if (!context.appliesTo(node)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return contexts.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ public class Annotations {
|
||||
|
||||
public static final String COMPONENT = "org.springframework.stereotype.Component";
|
||||
public static final String CONFIGURATION = "org.springframework.context.annotation.Configuration";
|
||||
public static final String CONTROLLER = "org.springframework.stereotype.Controller";
|
||||
|
||||
public static final String REPOSITORY = "org.springframework.stereotype.Repository";
|
||||
public static final String REPOSITORY_DEFINITION = "org.springframework.data.repository.RepositoryDefinition";
|
||||
public static final String NO_REPO_BEAN = "org.springframework.data.repository.NoRepositoryBean";
|
||||
|
||||
@@ -118,7 +118,7 @@ public abstract class AnnotationHierarchies {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMetaAnnotation(ITypeBinding candidate, Predicate<String> isKeyAnnotationName) {
|
||||
public static boolean isMetaAnnotation(ITypeBinding candidate, Predicate<String> isKeyAnnotationName) {
|
||||
return findTransitiveSupers(candidate, new HashSet<>())
|
||||
.anyMatch(sa -> isKeyAnnotationName.test(sa.getQualifiedName()));
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ import org.eclipse.jdt.core.dom.SimpleName;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
|
||||
public interface JavaSnippetContext {
|
||||
|
||||
JavaSnippetContext BOOT_MEMBERS = (node) -> node instanceof TypeDeclaration || node instanceof SimpleName;
|
||||
|
||||
JavaSnippetContext AT_ROOT_LEVEL = (node) -> {
|
||||
return (node instanceof TypeDeclaration) || (node instanceof SimpleName && node.getParent() != null && node.getParent() instanceof TypeDeclaration);
|
||||
};
|
||||
|
||||
|
||||
boolean appliesTo(ASTNode node);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@
|
||||
*******************************************************************************/
|
||||
package org.springframework.ide.vscode.boot.java.requestmapping.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import org.eclipse.lsp4j.CompletionItem;
|
||||
import org.eclipse.lsp4j.CompletionList;
|
||||
import org.eclipse.lsp4j.InsertTextMode;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,6 +37,9 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@Import(HoverTestConf.class)
|
||||
public class RequestMappingSnippetTests {
|
||||
|
||||
private static final String CONTROLLER_CLASSNAME = "SampleController";
|
||||
private static final String NON_CONTROLLER_CLASSNAME = "SampleNonController";
|
||||
|
||||
@Autowired private BootLanguageServerHarness harness;
|
||||
private Editor editor;
|
||||
|
||||
@@ -50,7 +52,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testAnnotationMarkerOnkyPrefix() throws Exception {
|
||||
prepareCase("@<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "@<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(4, completions.size());
|
||||
@@ -73,7 +75,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testSimpleGetPrefix() throws Exception {
|
||||
prepareCase("Get<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "Get<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(1, completions.size());
|
||||
@@ -84,7 +86,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testSimplePostPrefix() throws Exception {
|
||||
prepareCase("Post<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "Post<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(1, completions.size());
|
||||
@@ -95,7 +97,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testAnnotationAndTextPrefixForGet() throws Exception {
|
||||
prepareCase("@G<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "@G<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(1, completions.size());
|
||||
@@ -106,7 +108,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testAnnotationAndTextPrefixForGet2() throws Exception {
|
||||
prepareCase("@Get<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "@Get<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(1, completions.size());
|
||||
@@ -117,7 +119,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testAnnotationAndTextPrefixForGet3() throws Exception {
|
||||
prepareCase("@GetM<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "@GetM<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(1, completions.size());
|
||||
@@ -128,7 +130,7 @@ public class RequestMappingSnippetTests {
|
||||
|
||||
@Test
|
||||
void testSnippetExtractionIntoCode() throws Exception {
|
||||
prepareCase("Get<*>");
|
||||
prepareCase(CONTROLLER_CLASSNAME, "Get<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(1, completions.size());
|
||||
@@ -152,12 +154,20 @@ public class RequestMappingSnippetTests {
|
||||
+ "}\n"
|
||||
+ "");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSnippetsNotShowUpForNonControllerClasses() throws Exception {
|
||||
prepareCase(NON_CONTROLLER_CLASSNAME, "@<*>");
|
||||
|
||||
List<CompletionItem> completions = editor.getCompletions();
|
||||
assertEquals(0, completions.size());
|
||||
}
|
||||
|
||||
private void prepareCase(String prefix) throws Exception {
|
||||
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-request-mapping-completions/src/main/java/example/SampleController.java");
|
||||
private void prepareCase(String className, String prefix) throws Exception {
|
||||
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-request-mapping-completions/src/main/java/example/" + className + ".java");
|
||||
String content = IOUtils.toString(resource);
|
||||
|
||||
content = content.replace("class SampleController {", "class SampleController {\n\n" + prefix);
|
||||
content = content.replace("class " + className + " {", "class " + className + " {\n\n" + prefix);
|
||||
editor = new Editor(harness, content, LanguageId.JAVA);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package example;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SampleNonController {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user