Skip to content

Commit 24c5d4e

Browse files
committed
GH-1348: navigation for events improved
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
1 parent 12c50b5 commit 24c5d4e

File tree

9 files changed

+165
-58
lines changed

9 files changed

+165
-58
lines changed

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java

+19
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,25 @@ public DocumentElement getDocument(String docURI) {
9696
return null;
9797
}
9898

99+
public <T extends SpringIndexElement> List<T> getNodesOfType(Class<T> type) {
100+
List<T> result = new ArrayList<>();
101+
102+
ArrayDeque<SpringIndexElement> elementsToVisit = new ArrayDeque<>();
103+
elementsToVisit.addAll(this.projectRootElements.values());
104+
105+
while (!elementsToVisit.isEmpty()) {
106+
SpringIndexElement element = elementsToVisit.pop();
107+
108+
if (type.isInstance(element)) {
109+
result.add(type.cast(element));
110+
}
111+
112+
elementsToVisit.addAll(element.getChildren());
113+
}
114+
115+
return result;
116+
}
117+
99118
public Bean[] getBeans() {
100119
List<Bean> result = new ArrayList<>();
101120

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2017, 2018 Pivotal, Inc.
2+
* Copyright (c) 2017, 2025 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -23,6 +23,7 @@
2323
import org.springframework.ide.vscode.commons.util.Assert;
2424

2525
import com.google.common.collect.ImmutableList;
26+
import com.google.common.collect.ImmutableSet;
2627

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

9697
public Collection<T> getAll() {
97-
ImmutableList.Builder<T> found = ImmutableList.builder();
98+
ImmutableSet.Builder<T> found = ImmutableSet.builder();
9899
Collection<Binding<T>> values = bindings.values();
99100
values.forEach(binding -> found.add(binding.value));
100101
return found.build();

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java

+33-23
Original file line numberDiff line numberDiff line change
@@ -113,31 +113,41 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec
113113
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration);
114114

115115
// event listener - create child element, if necessary
116-
ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER));
117-
if (inTypeHierarchy != null) {
118-
119-
MethodDeclaration handleEventMethod = findHandleEventMethod(type);
120-
if (handleEventMethod != null) {
121-
122-
IMethodBinding methodBinding = handleEventMethod.resolveBinding();
123-
ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
124-
if (parameterTypes != null && parameterTypes.length == 1) {
125-
126-
ITypeBinding eventType = parameterTypes[0];
127-
String eventTypeFq = eventType.getQualifiedName();
128-
129-
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName());
130-
Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange());
131-
132-
Collection<Annotation> annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod);
133-
AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc);
134-
135-
EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, beanType.getQualifiedName(), handleEventMethodAnnotations);
136-
beanDefinition.addChild(eventElement);
137-
}
138-
}
116+
List<CachedBean> alreadyCreatedEventListenerChilds = context.getBeans().stream()
117+
.filter(cachedBean -> cachedBean.getDocURI().equals(doc.getUri()))
118+
.filter(cachedBean -> cachedBean.getBean() instanceof EventListenerIndexElement)
119+
.toList();
120+
121+
for (CachedBean eventListener : alreadyCreatedEventListenerChilds) {
122+
context.getBeans().remove(eventListener);
123+
beanDefinition.addChild(eventListener.getBean());
139124
}
140125

126+
// ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER));
127+
// if (inTypeHierarchy != null) {
128+
//
129+
// MethodDeclaration handleEventMethod = findHandleEventMethod(type);
130+
// if (handleEventMethod != null) {
131+
//
132+
// IMethodBinding methodBinding = handleEventMethod.resolveBinding();
133+
// ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
134+
// if (parameterTypes != null && parameterTypes.length == 1) {
135+
//
136+
// ITypeBinding eventType = parameterTypes[0];
137+
// String eventTypeFq = eventType.getQualifiedName();
138+
//
139+
// DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName());
140+
// Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange());
141+
//
142+
// Collection<Annotation> annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod);
143+
// AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc);
144+
//
145+
// EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, beanType.getQualifiedName(), handleEventMethodAnnotations);
146+
// beanDefinition.addChild(eventElement);
147+
// }
148+
// }
149+
// }
150+
141151
// event publisher checks
142152
for (InjectionPoint injectionPoint : injectionPoints) {
143153
if (Annotations.EVENT_PUBLISHER.equals(injectionPoint.getType())) {

Diff for: headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventReferenceProvider.java

+16-28
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
*******************************************************************************/
1111
package org.springframework.ide.vscode.boot.java.events;
1212

13-
import java.util.Arrays;
1413
import java.util.List;
1514
import java.util.Optional;
15+
import java.util.Set;
1616

1717
import org.eclipse.jdt.core.dom.ASTNode;
1818
import org.eclipse.jdt.core.dom.Annotation;
@@ -25,7 +25,6 @@
2525
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
2626
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
2727
import org.springframework.ide.vscode.commons.java.IJavaProject;
28-
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
2928
import org.springframework.ide.vscode.commons.util.BadLocationException;
3029
import org.springframework.ide.vscode.commons.util.text.TextDocument;
3130

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

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

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

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

8579
// when offset is inside an event publisher, find the respective event type
8680
else {
87-
Optional<String> publisherEventType = Arrays.stream(beans)
88-
.filter(bean -> bean.getLocation().getUri().equals(doc.getUri()))
89-
.flatMap(bean -> bean.getChildren().stream())
90-
.filter(element -> element instanceof EventPublisherIndexElement)
91-
.map(element -> (EventPublisherIndexElement) element)
92-
.filter(eventListener -> isPositionInside(position, eventListener.getLocation()))
93-
.map(eventListener -> eventListener.getEventType())
81+
Optional<EventPublisherIndexElement> publisherElement = publishers.stream()
82+
.filter(publisher -> publisher.getLocation().getUri().equals(doc.getUri()))
83+
.filter(eventPublisher -> isPositionInside(position, eventPublisher.getLocation()))
9484
.findAny();
9585

96-
if (publisherEventType.isPresent()) {
97-
// use the listener event type to look for publishers for that type
98-
String eventType = publisherEventType.get();
86+
if (publisherElement.isPresent()) {
87+
// use the publisher event type to look for listeners for that type
88+
String eventType = publisherElement.get().getEventType();
89+
Set<String> eventTypesFromHierarchy = publisherElement.get().getEventTypesFromHierarchy();
9990

100-
List<Location> foundLocations = Arrays.stream(beans)
101-
.flatMap(bean -> bean.getChildren().stream())
102-
.filter(element -> element instanceof EventListenerIndexElement)
103-
.map(element -> (EventListenerIndexElement) element)
104-
.filter(listener -> listener.getEventType().equals(eventType))
105-
.map(listener-> listener.getLocation())
91+
List<Location> foundLocations = listeners.stream()
92+
.filter(listener -> listener.getEventType().equals(eventType) || eventTypesFromHierarchy.contains(listener.getEventType()))
93+
.map(listener -> listener.getLocation())
10694
.toList();
10795

10896
if (foundLocations.size() > 0) {

Diff for: headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/EventsReferencesProviderTest.java

+45
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,49 @@ public void foo() {
131131
assertTrue(references.contains(expectedLocation1));
132132
}
133133

134+
@Test
135+
public void testEventPublisherFindsAllListenersIncludingThoseFromListenersWithoutAnnotation() throws Exception {
136+
String tempJavaDocUri = directory.toPath().resolve("src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java").toUri().toString();
137+
138+
Editor editor = harness.newEditor(LanguageId.JAVA, """
139+
package com.example.events.demo;
140+
141+
import org.springframework.context.ApplicationEventPublisher;
142+
import org.springframework.stereotype.Component;
143+
144+
@Component
145+
public class CustomApplcationEventPublisher {
146+
147+
private ApplicationEventPublisher publisher;
148+
149+
public CustomApplcationEventPublisher(ApplicationEventPublisher publisher) {
150+
this.publisher = publisher;
151+
}
152+
153+
public void foo() {
154+
this.publisher.pub<*>lishEvent(new CustomApplicationEvent(null));
155+
}
156+
}""", tempJavaDocUri);
157+
158+
List<? extends Location> references = editor.getReferences();
159+
assertEquals(3, references.size());
160+
161+
String expectedDefinitionUri1 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerInterface.java").toUri().toString();
162+
Location expectedLocation1 = new Location(expectedDefinitionUri1, new Range(new Position(10, 13), new Position(10, 31)));
163+
164+
assertTrue(references.contains(expectedLocation1));
165+
166+
String expectedDefinitionUri2 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerAnnotation.java").toUri().toString();
167+
Location expectedLocation2 = new Location(expectedDefinitionUri2, new Range(new Position(10, 13), new Position(10, 24)));
168+
169+
assertTrue(references.contains(expectedLocation2));
170+
171+
String expectedDefinitionUri3 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java").toUri().toString();
172+
Location expectedLocation3 = new Location(expectedDefinitionUri3, new Range(new Position(9, 13), new Position(9, 24)));
173+
174+
assertTrue(references.contains(expectedLocation3));
175+
176+
177+
}
178+
134179
}

Diff for: headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/SpringIndexerEventsTest.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import static org.junit.Assert.assertFalse;
1414
import static org.junit.Assert.assertNotNull;
15+
import static org.junit.Assert.assertSame;
1516
import static org.junit.Assert.assertTrue;
1617
import static org.junit.jupiter.api.Assertions.assertEquals;
1718

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

137+
DocumentElement document = springIndex.getDocument(docUri);
138+
List<SpringIndexElement> docChildren = document.getChildren();
139+
assertEquals(1, docChildren.size());
140+
assertTrue(docChildren.get(0) instanceof Bean);
141+
136142
Bean listenerComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("eventListenerPerInterface")).findFirst().get();
137143
assertEquals("com.example.events.demo.EventListenerPerInterface", listenerComponentBean.getType());
138144

@@ -148,6 +154,13 @@ void testEventListenerIndexElementForEventListenerInterfaceImplementation() thro
148154
assertNotNull(location);
149155
assertEquals(docUri, location.getUri());
150156
assertEquals(new Range(new Position(10, 13), new Position(10, 31)), location.getRange());
157+
158+
List<EventListenerIndexElement> doubleCheckEventListenerNodes = springIndex.getNodesOfType(EventListenerIndexElement.class).stream()
159+
.filter(eventListener -> eventListener.getLocation().getUri().equals(docUri))
160+
.toList();
161+
162+
assertEquals(1, doubleCheckEventListenerNodes.size());
163+
assertSame(listenerElement, doubleCheckEventListenerNodes.get(0));
151164
}
152165

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

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

167180
Location location = listenerElement.getLocation();
168181
assertNotNull(location);
169182
assertEquals(docUri, location.getUri());
170-
assertEquals(new Range(new Position(8, 13), new Position(8, 31)), location.getRange());
183+
assertEquals(new Range(new Position(7, 13), new Position(7, 31)), location.getRange());
171184
}
172185

173186
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.example.events.demo;
2+
3+
import org.springframework.context.ApplicationEvent;
4+
5+
public class CustomApplicationEvent extends ApplicationEvent {
6+
7+
private static final long serialVersionUID = 1L;
8+
9+
public CustomApplicationEvent(Object source) {
10+
super(source);
11+
}
12+
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.example.events.demo;
2+
3+
import org.springframework.context.ApplicationEventPublisher;
4+
import org.springframework.stereotype.Component;
5+
6+
@Component
7+
public class CustomApplicationEventPublisher {
8+
9+
private ApplicationEventPublisher publisher;
10+
11+
public CustomApplicationEventPublisher(ApplicationEventPublisher publisher) {
12+
this.publisher = publisher;
13+
}
14+
15+
public void foo() {
16+
this.publisher.publishEvent(new CustomApplicationEvent(null));
17+
}
18+
19+
}

Diff for: headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package com.example.events.demo;
22

3-
import org.springframework.context.ApplicationEvent;
43
import org.springframework.context.ApplicationListener;
54

6-
public class EventListenerPerInterfaceAndBeanMethod implements ApplicationListener<ApplicationEvent> {
5+
public class EventListenerPerInterfaceAndBeanMethod implements ApplicationListener<CustomApplicationEvent> {
76

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

0 commit comments

Comments
 (0)