diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index abd30b268..1064b54fb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -99,6 +99,10 @@ public class Annotations { public static final String APPLICATION_LISTENER = "org.springframework.context.ApplicationListener"; public static final String EVENT_PUBLISHER = "org.springframework.context.ApplicationEventPublisher"; + public static final String BEAN_REGISTRAR_INTERFACE = "org.springframework.beans.factory.BeanRegistrar"; + public static final String BEAN_REGISTRY_INTERFACE = "org.springframework.beans.factory.BeanRegistry"; + + public static final Map AOP_ANNOTATIONS = Map.of( "org.aspectj.lang.annotation.Pointcut", "Pointcut", "org.aspectj.lang.annotation.Before", "Before", @@ -108,7 +112,6 @@ public class Annotations { "org.aspectj.lang.annotation.AfterThrowing", "AfterThrowing", "org.aspectj.lang.annotation.DeclareParents", "DeclareParents" ); - - + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index 138b6c0dc..cdf095c5b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.beans; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -18,9 +19,11 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.Block; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; @@ -140,6 +143,7 @@ public class ComponentSymbolProvider implements SymbolProvider { indexEventListenerInterfaceImplementation(beanDefinition, type, context, doc); indexRequestMappings(beanDefinition, type, annotationType, metaAnnotations, context, doc); indexConfigurationProperties(beanDefinition, type, context, doc); + indexBeanRegistrarImplementation(beanDefinition, type, context, doc); context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); @@ -321,6 +325,7 @@ public class ComponentSymbolProvider implements SymbolProvider { // check for event listener implementations on classes that are not annotated with component, but created via bean methods (for example) if (!isComponment) { indexEventListenerInterfaceImplementation(null, typeDeclaration, context, doc); + indexBeanRegistrarImplementation(null, typeDeclaration, context, doc); } } @@ -377,6 +382,191 @@ public class ComponentSymbolProvider implements SymbolProvider { return null; } + private MethodDeclaration findRegisterMethod(TypeDeclaration type, ITypeBinding beanRegistrarType) { + IMethodBinding[] beanRegistrarMethods = beanRegistrarType.getDeclaredMethods(); + if (beanRegistrarMethods == null || beanRegistrarMethods.length != 1 || !"register".equals(beanRegistrarMethods[0].getName())) { + return null; + } + + MethodDeclaration[] methods = type.getMethods(); + + for (MethodDeclaration method : methods) { + IMethodBinding binding = method.resolveBinding(); + boolean overrides = binding.overrides(beanRegistrarMethods[0]); + if (overrides) { + return method; + } + } + + return null; + } + + private void indexBeanRegistrarImplementation(Bean bean, TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) { + try { + ITypeBinding typeBinding = typeDeclaration.resolveBinding(); + if (typeBinding == null) return; + + ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(typeDeclaration, doc, typeBinding, Set.of(Annotations.BEAN_REGISTRAR_INTERFACE)); + if (inTypeHierarchy == null) return; + + MethodDeclaration registerMethod = findRegisterMethod(typeDeclaration, inTypeHierarchy); + if (registerMethod == null) return; + + if (!context.isFullAst()) { // needs full method bodies to continue + throw new RequiredCompleteAstException(); + } + + if (bean == null) { // need to create and register bean element + String beanType = typeBinding.getQualifiedName(); + String beanName = BeanUtils.getBeanNameFromType(typeBinding.getName()); + + Location location = new Location(doc.getUri(), doc.toRange(typeDeclaration.getStartPosition(), typeDeclaration.getLength())); + + WorkspaceSymbol symbol = new WorkspaceSymbol( + beanLabel("+", null, null, beanName, beanType), + SymbolKind.Class, + Either.forLeft(location)); + + InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(typeDeclaration, doc); + + Set supertypes = new HashSet<>(); + ASTUtils.findSupertypes(typeBinding, supertypes); + + Collection annotationsOnMethod = ASTUtils.getAnnotations(typeDeclaration); + AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc); + + bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, false, symbol.getName()); + + context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); + context.getBeans().add(new CachedBean(context.getDocURI(), bean)); + } + + scanBeanRegistryInvocations(bean, registerMethod.getBody(), context, doc); + + } catch (BadLocationException e) { + log.error("", e); + } + } + + private void scanBeanRegistryInvocations(Bean component, Block body, SpringIndexerJavaContext context, TextDocument doc) { + if (body == null) { + return; + } + + body.accept(new ASTVisitor() { + + @Override + public boolean visit(MethodInvocation methodInvocation) { + try { + String methodName = methodInvocation.getName().toString(); + if ("registerBean".equals(methodName)) { + + IMethodBinding methodBinding = methodInvocation.resolveMethodBinding(); + ITypeBinding declaringClass = methodBinding.getDeclaringClass(); + + if (declaringClass != null && Annotations.BEAN_REGISTRY_INTERFACE.equals(declaringClass.getQualifiedName())) { + + @SuppressWarnings("unchecked") + List arguments = methodInvocation.arguments(); + List types = new ArrayList<>(); + + for (Expression argument : arguments) { + ITypeBinding typeBinding = argument.resolveTypeBinding(); + if (typeBinding != null) { + types.add(typeBinding); + } + else { + return true; + } + } + + if (arguments.size() == 1 && "java.lang.Class".equals(types.get(0).getBinaryName())) { + // String registerBean(Class beanClass); + + ITypeBinding typeBinding = types.get(0); + ITypeBinding[] typeParameters = typeBinding.getTypeArguments(); + if (typeParameters != null && typeParameters.length == 1) { + String typeParamName = typeParameters[0].getBinaryName(); + + String beanName = BeanUtils.getBeanNameFromType(typeParameters[0].getName()); + String beanType = typeParamName; + + createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc); + } + } + else if (arguments.size() == 2 && "java.lang.String".equals(types.get(0).getQualifiedName()) && "java.lang.Class".equals(types.get(1).getBinaryName())) { + // void registerBean(String name, Class beanClass); + + String beanName = ASTUtils.getExpressionValueAsString(arguments.get(0), (dep) -> {}); + + ITypeBinding typeBinding = types.get(1); + ITypeBinding[] typeParameters = typeBinding.getTypeArguments(); + if (typeParameters != null && typeParameters.length == 1) { + String typeParamName = typeParameters[0].getBinaryName(); + String beanType = typeParamName; + + createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc); + } + } + else if (arguments.size() == 2 && "java.lang.Class".equals(types.get(0).getBinaryName()) && "java.util.function.Consumer".equals(types.get(1).getBinaryName())) { + // String registerBean(Class beanClass, Consumer> customizer); + + ITypeBinding typeBinding = types.get(0); + ITypeBinding[] typeParameters = typeBinding.getTypeArguments(); + if (typeParameters != null && typeParameters.length == 1) { + String typeParamName = typeParameters[0].getBinaryName(); + + String beanName = BeanUtils.getBeanNameFromType(typeParameters[0].getName()); + String beanType = typeParamName; + + createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc); + } + } + else if (arguments.size() == 3 && "java.lang.String".equals(types.get(0).getQualifiedName()) + && "java.lang.Class".equals(types.get(1).getBinaryName()) && "java.util.function.Consumer".equals(types.get(2).getBinaryName())) { + // void registerBean(String name, Class beanClass, Consumer> customizer); + + String beanName = ASTUtils.getExpressionValueAsString(arguments.get(0), (dep) -> {}); + + ITypeBinding typeBinding = types.get(1); + ITypeBinding[] typeParameters = typeBinding.getTypeArguments(); + if (typeParameters != null && typeParameters.length == 1) { + String typeParamName = typeParameters[0].getBinaryName(); + String beanType = typeParamName; + + createBean(component, beanName, beanType, typeParameters[0], methodInvocation, context, doc); + } + } + } + } + + } catch (BadLocationException e) { + log.error("", e); + } + return super.visit(methodInvocation); + } + }); + } + + public void createBean(Bean parentBean, String beanName, String beanType, ITypeBinding beanTypeBinding, ASTNode node, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException { + Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength())); + + WorkspaceSymbol symbol = new WorkspaceSymbol( + beanLabel("+", null, null, beanName, beanType), + SymbolKind.Class, + Either.forLeft(location)); + context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); + + InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS; + Set supertypes = new HashSet<>(); + ASTUtils.findSupertypes(beanTypeBinding, supertypes); + + AnnotationMetadata[] annotations = DefaultValues.EMPTY_ANNOTATIONS; + + Bean bean = new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations, false, symbol.getName()); + parentBean.addChild(bean); + } + public static String beanLabel(String searchPrefix, String annotationTypeName, Collection metaAnnotationNames, String beanName, String beanType) { StringBuilder symbolLabel = new StringBuilder(); symbolLabel.append("@"); @@ -385,21 +575,25 @@ public class ComponentSymbolProvider implements SymbolProvider { symbolLabel.append('\''); symbolLabel.append(beanName); symbolLabel.append('\''); - symbolLabel.append(" (@"); - symbolLabel.append(annotationTypeName); - if (!metaAnnotationNames.isEmpty()) { - symbolLabel.append(" <: "); - boolean first = true; - for (String ma : metaAnnotationNames) { - if (!first) { - symbolLabel.append(", "); + + if (annotationTypeName != null) { + symbolLabel.append(" (@"); + symbolLabel.append(annotationTypeName); + if (!metaAnnotationNames.isEmpty()) { + symbolLabel.append(" <: "); + boolean first = true; + for (String ma : metaAnnotationNames) { + if (!first) { + symbolLabel.append(", "); + } + symbolLabel.append("@"); + symbolLabel.append(ma); + first = false; } - symbolLabel.append("@"); - symbolLabel.append(ma); - first = false; } + symbolLabel.append(")"); } - symbolLabel.append(") "); + symbolLabel.append(" "); symbolLabel.append(beanType); return symbolLabel.toString(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index c54998126..3a5548a5c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -403,7 +403,7 @@ public class ASTUtils { simplifiedType = resolvedInterface.getBinaryName(); } else { - simplifiedType = resolvedType.getQualifiedName(); + simplifiedType = resolvedInterface.getQualifiedName(); } if (typesToCheck.contains(simplifiedType)) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerBeanRegistrarTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerBeanRegistrarTest.java new file mode 100644 index 000000000..901622feb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringIndexerBeanRegistrarTest.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2025 Broadcom + * 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.index.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class SpringIndexerBeanRegistrarTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + @Autowired private SpringMetamodelIndex springIndex; + + private File directory; + + @BeforeEach + public void setup() throws Exception { + harness.intialize(null); + + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-framework-7-indexing/").toURI()); + + String projectDir = directory.toURI().toString(); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void testSimpleBeanRegistration() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/com/example/MyBeanRegistrar.java").toUri().toString(); + + Bean[] beans = springIndex.getBeansOfDocument(docUri); + assertEquals(5, beans.length); + + Bean beanRegistrarBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("myBeanRegistrar")).findFirst().get(); + assertEquals("com.example.MyBeanRegistrar", beanRegistrarBean.getType()); + + List children = beanRegistrarBean.getChildren(); + assertEquals(4, children.size()); + + Bean fooFoo = (Bean) children.get(0); + assertEquals("fooFoo", fooFoo.getName()); + assertEquals("com.example.FooFoo", fooFoo.getType()); + + Bean foo = (Bean) children.get(1); + assertEquals("foo", foo.getName()); + assertEquals("com.example.Foo", foo.getType()); + + Bean bar = (Bean) children.get(2); + assertEquals("bar", bar.getName()); + assertEquals("com.example.Bar", bar.getType()); + + Bean baz = (Bean) children.get(3); + assertEquals("baz", baz.getName()); + assertEquals("com.example.Baz", baz.getType()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/MyBeanRegistrar.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/MyBeanRegistrar.java index 5244306c1..202cbb8e4 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/MyBeanRegistrar.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-framework-7-indexing/src/main/java/com/example/MyBeanRegistrar.java @@ -6,19 +6,19 @@ import org.springframework.core.env.Environment; public class MyBeanRegistrar implements BeanRegistrar { - @Override - public void register(BeanRegistry registry, Environment env) { - registry.registerBean(FooFoo.class); - registry.registerBean("foo", Foo.class); - registry.registerBean("bar", Bar.class, spec -> spec - .prototype() - .lazyInit() - .description("Custom description") - .supplier(context -> new Bar(context.bean(Foo.class)))); - if (env.matchesProfiles("baz")) { - registry.registerBean(Baz.class, spec -> spec - .supplier(context -> new Baz("Hello World!"))); + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(FooFoo.class); + registry.registerBean("foo", Foo.class); + registry.registerBean("bar", Bar.class, spec -> spec + .prototype() + .lazyInit() + .description("Custom description") + .supplier(context -> new Bar(context.bean(Foo.class)))); + if (env.matchesProfiles("baz")) { + registry.registerBean(Baz.class, spec -> spec + .supplier(context -> new Baz("Hello World!"))); + } } - } }