Skip to content

Commit

Permalink
GH-1348: navigation for events improved
Browse files Browse the repository at this point in the history
GH-1461: hierarchy of events taken into account when looking for references from listeners, too

GH-1462: event listener index nodes now not created twice for type and annotation anymore
  • Loading branch information
martinlippert committed Feb 6, 2025
1 parent 4f675cc commit 0b6b6df
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,25 @@ public DocumentElement getDocument(String docURI) {
return null;
}

public <T extends SpringIndexElement> List<T> getNodesOfType(Class<T> type) {
List<T> result = new ArrayList<>();

ArrayDeque<SpringIndexElement> elementsToVisit = new ArrayDeque<>();
elementsToVisit.addAll(this.projectRootElements.values());

while (!elementsToVisit.isEmpty()) {
SpringIndexElement element = elementsToVisit.pop();

if (type.isInstance(element)) {
result.add(type.cast(element));
}

elementsToVisit.addAll(element.getChildren());
}

return result;
}

public Bean[] getBeans() {
List<Bean> result = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2018 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
Expand All @@ -23,6 +23,7 @@
import org.springframework.ide.vscode.commons.util.Assert;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

/**
* A Map-like utilty that allows putting and getting values associated with
Expand Down Expand Up @@ -94,7 +95,7 @@ public Collection<T> get(AnnotationHierarchies annotationHierarchies, IAnnotatio
}

public Collection<T> getAll() {
ImmutableList.Builder<T> found = ImmutableList.builder();
ImmutableSet.Builder<T> found = ImmutableSet.builder();
Collection<Binding<T>> values = bindings.values();
values.forEach(binding -> found.add(binding.value));
return found.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,31 +113,41 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration);

// event listener - create child element, if necessary
ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER));
if (inTypeHierarchy != null) {

MethodDeclaration handleEventMethod = findHandleEventMethod(type);
if (handleEventMethod != null) {

IMethodBinding methodBinding = handleEventMethod.resolveBinding();
ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
if (parameterTypes != null && parameterTypes.length == 1) {

ITypeBinding eventType = parameterTypes[0];
String eventTypeFq = eventType.getQualifiedName();

DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName());
Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange());

Collection<Annotation> annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod);
AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc);

EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, beanType.getQualifiedName(), handleEventMethodAnnotations);
beanDefinition.addChild(eventElement);
}
}
List<CachedBean> alreadyCreatedEventListenerChilds = context.getBeans().stream()
.filter(cachedBean -> cachedBean.getDocURI().equals(doc.getUri()))
.filter(cachedBean -> cachedBean.getBean() instanceof EventListenerIndexElement)
.toList();

for (CachedBean eventListener : alreadyCreatedEventListenerChilds) {
context.getBeans().remove(eventListener);
beanDefinition.addChild(eventListener.getBean());
}

// ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER));
// if (inTypeHierarchy != null) {
//
// MethodDeclaration handleEventMethod = findHandleEventMethod(type);
// if (handleEventMethod != null) {
//
// IMethodBinding methodBinding = handleEventMethod.resolveBinding();
// ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
// if (parameterTypes != null && parameterTypes.length == 1) {
//
// ITypeBinding eventType = parameterTypes[0];
// String eventTypeFq = eventType.getQualifiedName();
//
// DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName());
// Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange());
//
// Collection<Annotation> annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod);
// AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc);
//
// EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, beanType.getQualifiedName(), handleEventMethodAnnotations);
// beanDefinition.addChild(eventElement);
// }
// }
// }

// event publisher checks
for (InjectionPoint injectionPoint : injectionPoints) {
if (Annotations.EVENT_PUBLISHER.equals(injectionPoint.getType())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.events;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
Expand All @@ -25,7 +25,6 @@
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.TextDocument;

Expand Down Expand Up @@ -53,14 +52,12 @@ public List<? extends Location> provideReferences(CancelChecker cancelToken, IJa
try {
Position position = doc.toPosition(offset);

Bean[] beans = index.getBeans();
List<EventListenerIndexElement> listeners = index.getNodesOfType(EventListenerIndexElement.class);
List<EventPublisherIndexElement> publishers = index.getNodesOfType(EventPublisherIndexElement.class);

// when offset is inside an event listener, find the respective event type
Optional<String> listenerEventType = Arrays.stream(beans)
.filter(bean -> bean.getLocation().getUri().equals(doc.getUri()))
.flatMap(bean -> bean.getChildren().stream())
.filter(element -> element instanceof EventListenerIndexElement)
.map(element -> (EventListenerIndexElement) element)
Optional<String> listenerEventType = listeners.stream()
.filter(listener -> listener.getLocation().getUri().equals(doc.getUri()))
.filter(eventListener -> isPositionInside(position, eventListener.getLocation()))
.map(eventListener -> eventListener.getEventType())
.findAny();
Expand All @@ -69,10 +66,7 @@ public List<? extends Location> provideReferences(CancelChecker cancelToken, IJa
// use the listener event type to look for publishers for that type
String eventType = listenerEventType.get();

List<Location> foundLocations = Arrays.stream(beans)
.flatMap(bean -> bean.getChildren().stream())
.filter(element -> element instanceof EventPublisherIndexElement)
.map(element -> (EventPublisherIndexElement) element)
List<Location> foundLocations = publishers.stream()
.filter(publisher -> publisher.getEventType().equals(eventType) || publisher.getEventTypesFromHierarchy().contains(eventType))
.map(publisher -> publisher.getLocation())
.toList();
Expand All @@ -84,25 +78,19 @@ public List<? extends Location> provideReferences(CancelChecker cancelToken, IJa

// when offset is inside an event publisher, find the respective event type
else {
Optional<String> publisherEventType = Arrays.stream(beans)
.filter(bean -> bean.getLocation().getUri().equals(doc.getUri()))
.flatMap(bean -> bean.getChildren().stream())
.filter(element -> element instanceof EventPublisherIndexElement)
.map(element -> (EventPublisherIndexElement) element)
.filter(eventListener -> isPositionInside(position, eventListener.getLocation()))
.map(eventListener -> eventListener.getEventType())
Optional<EventPublisherIndexElement> publisherElement = publishers.stream()
.filter(publisher -> publisher.getLocation().getUri().equals(doc.getUri()))
.filter(eventPublisher -> isPositionInside(position, eventPublisher.getLocation()))
.findAny();

if (publisherEventType.isPresent()) {
// use the listener event type to look for publishers for that type
String eventType = publisherEventType.get();
if (publisherElement.isPresent()) {
// use the publisher event type to look for listeners for that type
String eventType = publisherElement.get().getEventType();
Set<String> eventTypesFromHierarchy = publisherElement.get().getEventTypesFromHierarchy();

List<Location> foundLocations = Arrays.stream(beans)
.flatMap(bean -> bean.getChildren().stream())
.filter(element -> element instanceof EventListenerIndexElement)
.map(element -> (EventListenerIndexElement) element)
.filter(listener -> listener.getEventType().equals(eventType))
.map(listener-> listener.getLocation())
List<Location> foundLocations = listeners.stream()
.filter(listener -> listener.getEventType().equals(eventType) || eventTypesFromHierarchy.contains(listener.getEventType()))
.map(listener -> listener.getLocation())
.toList();

if (foundLocations.size() > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,49 @@ public void foo() {
assertTrue(references.contains(expectedLocation1));
}

@Test
public void testEventPublisherFindsAllListenersIncludingThoseFromListenersWithoutAnnotation() throws Exception {
String tempJavaDocUri = directory.toPath().resolve("src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java").toUri().toString();

Editor editor = harness.newEditor(LanguageId.JAVA, """
package com.example.events.demo;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
public class CustomApplcationEventPublisher {
private ApplicationEventPublisher publisher;
public CustomApplcationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void foo() {
this.publisher.pub<*>lishEvent(new CustomApplicationEvent(null));
}
}""", tempJavaDocUri);

List<? extends Location> references = editor.getReferences();
assertEquals(3, references.size());

String expectedDefinitionUri1 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerInterface.java").toUri().toString();
Location expectedLocation1 = new Location(expectedDefinitionUri1, new Range(new Position(10, 13), new Position(10, 31)));

assertTrue(references.contains(expectedLocation1));

String expectedDefinitionUri2 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerAnnotation.java").toUri().toString();
Location expectedLocation2 = new Location(expectedDefinitionUri2, new Range(new Position(10, 13), new Position(10, 24)));

assertTrue(references.contains(expectedLocation2));

String expectedDefinitionUri3 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java").toUri().toString();
Location expectedLocation3 = new Location(expectedDefinitionUri3, new Range(new Position(9, 13), new Position(9, 24)));

assertTrue(references.contains(expectedLocation3));


}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;

Expand Down Expand Up @@ -133,6 +134,11 @@ void testEventListenerIndexElementForEventListenerInterfaceImplementation() thro
Bean[] beans = springIndex.getBeansOfDocument(docUri);
assertEquals(1, beans.length);

DocumentElement document = springIndex.getDocument(docUri);
List<SpringIndexElement> docChildren = document.getChildren();
assertEquals(1, docChildren.size());
assertTrue(docChildren.get(0) instanceof Bean);

Bean listenerComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("eventListenerPerInterface")).findFirst().get();
assertEquals("com.example.events.demo.EventListenerPerInterface", listenerComponentBean.getType());

Expand All @@ -148,6 +154,13 @@ void testEventListenerIndexElementForEventListenerInterfaceImplementation() thro
assertNotNull(location);
assertEquals(docUri, location.getUri());
assertEquals(new Range(new Position(10, 13), new Position(10, 31)), location.getRange());

List<EventListenerIndexElement> doubleCheckEventListenerNodes = springIndex.getNodesOfType(EventListenerIndexElement.class).stream()
.filter(eventListener -> eventListener.getLocation().getUri().equals(docUri))
.toList();

assertEquals(1, doubleCheckEventListenerNodes.size());
assertSame(listenerElement, doubleCheckEventListenerNodes.get(0));
}

@Test
Expand All @@ -161,13 +174,13 @@ void testEventListenerIndexElementForListenerInterfaceImplementationWithoutCompo
List<SpringIndexElement> children = document.getChildren();

EventListenerIndexElement listenerElement = children.stream().filter(element -> element instanceof EventListenerIndexElement).map(element -> (EventListenerIndexElement) element).findFirst().get();
assertEquals("org.springframework.context.ApplicationEvent", listenerElement.getEventType());
assertEquals("com.example.events.demo.CustomApplicationEvent", listenerElement.getEventType());
assertEquals("com.example.events.demo.EventListenerPerInterfaceAndBeanMethod", listenerElement.getContainerBeanType());

Location location = listenerElement.getLocation();
assertNotNull(location);
assertEquals(docUri, location.getUri());
assertEquals(new Range(new Position(8, 13), new Position(8, 31)), location.getRange());
assertEquals(new Range(new Position(7, 13), new Position(7, 31)), location.getRange());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.events.demo;

import org.springframework.context.ApplicationEvent;

public class CustomApplicationEvent extends ApplicationEvent {

private static final long serialVersionUID = 1L;

public CustomApplicationEvent(Object source) {
super(source);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.events.demo;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class CustomApplicationEventPublisher {

private ApplicationEventPublisher publisher;

public CustomApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

public void foo() {
this.publisher.publishEvent(new CustomApplicationEvent(null));
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.example.events.demo;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

public class EventListenerPerInterfaceAndBeanMethod implements ApplicationListener<ApplicationEvent> {
public class EventListenerPerInterfaceAndBeanMethod implements ApplicationListener<CustomApplicationEvent> {

@Override
public void onApplicationEvent(ApplicationEvent event) {
public void onApplicationEvent(CustomApplicationEvent event) {
System.out.println("Event received via listener implementation and bean method: " + event);
}

Expand Down

0 comments on commit 0b6b6df

Please sign in to comment.