From a40fd218baa712c6b060abd9d29c66cfd0f97696 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 30 Jan 2025 14:56:31 +0100 Subject: [PATCH] GH-1348: identifying event publishers now while indexing --- .../ide/vscode/boot/java/Annotations.java | 4 +- .../java/beans/ComponentSymbolProvider.java | 79 +++++++++++++++++++ .../events/EventPublisherIndexElement.java | 37 +++++++++ .../beans/test/SpringIndexerEventsTest.java | 33 ++++++++ 4 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventPublisherIndexElement.java 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 43ee68598..07d14d7e3 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2023 Pivotal, Inc. + * Copyright (c) 2017, 2025 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 @@ -90,7 +90,9 @@ public class Annotations { public static final String SCHEDULED = "org.springframework.scheduling.annotation.Scheduled"; public static final String EVENT_LISTENER = "org.springframework.context.event.EventListener"; + public static final String APPLICATION_LISTENER = "org.springframework.context.ApplicationListener"; + public static final String EVENT_PUBLISHER = "org.springframework.context.ApplicationEventPublisher"; public static final Map AOP_ANNOTATIONS = Map.of( "org.aspectj.lang.annotation.Pointcut", "Pointcut", 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 788984b0a..ae5be1d2b 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 @@ -13,14 +13,19 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolKind; @@ -30,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement; +import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement; import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider; import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; @@ -130,10 +136,65 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { } } } + + // event publisher checks + for (InjectionPoint injectionPoint : injectionPoints) { + if (Annotations.EVENT_PUBLISHER.equals(injectionPoint.getType())) { + context.getNextPassFiles().add(context.getFile()); + } + } context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), new EnhancedSymbolInformation(symbol))); context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition)); } + + @Override + protected void addSymbolsPass2(Annotation node, ITypeBinding annotationType, Collection metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) { + TypeDeclaration type = (TypeDeclaration) node.getParent(); + type.accept(new ASTVisitor() { + + @Override + public boolean visit(MethodInvocation methodInvocation) { + try { + String methodName = methodInvocation.getName().toString(); + if ("publishEvent".equals(methodName)) { + + IMethodBinding methodBinding = methodInvocation.resolveMethodBinding(); + boolean doesInvokeEventPublisher = Annotations.EVENT_PUBLISHER.equals(methodBinding.getDeclaringClass().getQualifiedName()); + if (doesInvokeEventPublisher) { + List arguments = methodInvocation.arguments(); + if (arguments != null && arguments.size() == 1) { + + ITypeBinding eventTypeBinding = ((Expression) arguments.get(0)).resolveTypeBinding(); + if (eventTypeBinding != null) { + + DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, methodInvocation); + Location location; + location = new Location(doc.getUri(), nodeRegion.asRange()); + + EventPublisherIndexElement eventPublisherIndexElement = new EventPublisherIndexElement(eventTypeBinding.getQualifiedName(), location); + Bean publisherBeanElement = findBean(node, methodInvocation, context, doc); + if (publisherBeanElement != null) { + publisherBeanElement.addChild(eventPublisherIndexElement); + } + + // symbol + String symbolLabel = "@EventPublisher (" + eventTypeBinding.getName() + ")"; + WorkspaceSymbol symbol = new WorkspaceSymbol(symbolLabel, SymbolKind.Interface, Either.forLeft(location)); + EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol); + context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol)); + } + } + } + } + + } catch (BadLocationException e) { + log.error("", e); + } + return super.visit(methodInvocation); + } + }); + } private MethodDeclaration findHandleEventMethod(TypeDeclaration type) { MethodDeclaration[] methods = type.getMethods(); @@ -148,6 +209,24 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider { } return null; } + + private Bean findBean(Annotation annotation, MethodInvocation methodInvocation, SpringIndexerJavaContext context, TextDocument doc) { + TypeDeclaration declaringType = ASTUtils.findDeclaringType(methodInvocation); + if (declaringType != null) { + String beanName = BeanUtils.getBeanNameFromComponentAnnotation(annotation, declaringType); + if (beanName != null) { + Optional first = context.getBeans().stream().filter(cachedBean -> cachedBean.getDocURI().equals(doc.getUri())) + .map(cachedBean -> cachedBean.getBean()) + .filter(bean -> bean instanceof Bean) + .map(bean -> (Bean) bean) + .filter(bean -> bean.getName().equals(beanName)) + .findFirst(); + return first.get(); + } + } + + return null; + } protected String beanLabel(String searchPrefix, String annotationTypeName, Collection metaAnnotationNames, String beanName, String beanType) { StringBuilder symbolLabel = new StringBuilder(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventPublisherIndexElement.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventPublisherIndexElement.java new file mode 100644 index 000000000..efe94ff98 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventPublisherIndexElement.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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.java.events; + +import org.eclipse.lsp4j.Location; +import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement; + +/** + * @author Martin Lippert + */ +public class EventPublisherIndexElement extends AbstractSpringIndexElement { + + private final String eventType; + private final Location location; + + public EventPublisherIndexElement(String eventType, Location location) { + this.eventType = eventType; + this.location = location; + } + + public String getEventType() { + return eventType; + } + + public Location getLocation() { + return location; + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerEventsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerEventsTest.java index 7b45055fd..08cef94ea 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerEventsTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerEventsTest.java @@ -34,6 +34,7 @@ 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.boot.java.events.EventListenerIndexElement; +import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement; 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; @@ -87,6 +88,15 @@ public class SpringIndexerEventsTest { SpringIndexerHarness.assertDocumentSymbols(indexer, docUri, SpringIndexerHarness.symbol("@Component", "@+ 'eventListenerPerInterface' (@Component) EventListenerPerInterface")); } + + @Test + void testEventPublisherSymbol() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/com/example/events/demo/CustomEventPublisher.java").toUri().toString(); + + SpringIndexerHarness.assertDocumentSymbols(indexer, docUri, + SpringIndexerHarness.symbol("@Component", "@+ 'customEventPublisher' (@Component) CustomEventPublisher"), + SpringIndexerHarness.symbol("this.publisher.publishEvent(new CustomEvent())", "@EventPublisher (CustomEvent)")); + } @Test void testAnnotationBasedEventListenerIndexElements() throws Exception { @@ -134,4 +144,27 @@ public class SpringIndexerEventsTest { assertEquals(new Range(new Position(10, 13), new Position(10, 31)), location.getRange()); } + @Test + void testEventPublisherIndexElements() throws Exception { + String docUri = directory.toPath().resolve("src/main/java/com/example/events/demo/CustomEventPublisher.java").toUri().toString(); + + Bean[] beans = springIndex.getBeansOfDocument(docUri); + assertEquals(1, beans.length); + + Bean listenerComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("customEventPublisher")).findFirst().get(); + assertEquals("com.example.events.demo.CustomEventPublisher", listenerComponentBean.getType()); + + List children = listenerComponentBean.getChildren(); + assertEquals(1, children.size()); + assertTrue(children.get(0) instanceof EventPublisherIndexElement); + + EventPublisherIndexElement publisherElement = (EventPublisherIndexElement) children.get(0); + assertEquals("com.example.events.demo.CustomEvent", publisherElement.getEventType()); + + Location location = publisherElement.getLocation(); + assertNotNull(location); + assertEquals(docUri, location.getUri()); + assertEquals(new Range(new Position(15, 2), new Position(15, 48)), location.getRange()); + } + }