Skip to content

Commit a835804

Browse files
committed
GH-1348: first steps towards indexing event listeners
1 parent 23eff6d commit a835804

24 files changed

+1042
-89
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider;
2020
import org.springframework.ide.vscode.boot.java.beans.FeignClientSymbolProvider;
2121
import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvider;
22+
import org.springframework.ide.vscode.boot.java.events.EventListenerSymbolProvider;
2223
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
2324
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider;
2425
import org.springframework.ide.vscode.boot.java.utils.RestrictedDefaultSymbolProvider;
@@ -35,6 +36,7 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache)
3536
ComponentSymbolProvider componentSymbolProvider = new ComponentSymbolProvider();
3637
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();
3738
DataRepositorySymbolProvider dataRepositorySymbolProvider = new DataRepositorySymbolProvider();
39+
EventListenerSymbolProvider eventListenerSymbolProvider = new EventListenerSymbolProvider();
3840

3941
providers.put(Annotations.SPRING_REQUEST_MAPPING, requestMappingSymbolProvider);
4042
providers.put(Annotations.SPRING_GET_MAPPING, requestMappingSymbolProvider);
@@ -70,6 +72,7 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache)
7072
providers.put(Annotations.CONDITIONAL_ON_SINGLE_CANDIDATE, restrictedDefaultSymbolProvider);
7173

7274
providers.put(Annotations.REPOSITORY, dataRepositorySymbolProvider);
75+
providers.put(Annotations.EVENT_LISTENER, eventListenerSymbolProvider);
7376

7477
providers.put(Annotations.FEIGN_CLIENT, new FeignClientSymbolProvider());
7578

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

+2
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public class Annotations {
8989
public static final Set<String> NAMED_ANNOTATIONS = Set.of(Annotations.NAMED_JAKARTA, Annotations.NAMED_JAVAX);
9090

9191
public static final String SCHEDULED = "org.springframework.scheduling.annotation.Scheduled";
92+
public static final String EVENT_LISTENER = "org.springframework.context.event.EventListener";
93+
public static final String APPLICATION_LISTENER = "org.springframework.context.ApplicationListener";
9294

9395
public static final Map<String, String> AOP_ANNOTATIONS = Map.of(
9496
"org.aspectj.lang.annotation.Pointcut", "Pointcut",

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

+5
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,10 @@ public static String getBeanNameFromType(String typeName) {
7777
}
7878
return typeName;
7979
}
80+
81+
public static String getBeanName(TypeDeclaration typeDeclaration) {
82+
String beanName = typeDeclaration.getName().toString();
83+
return BeanUtils.getBeanNameFromType(beanName);
84+
}
8085

8186
}

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

+4-5
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
import com.google.common.collect.ImmutableList;
5454

5555
import reactor.util.function.Tuple2;
56-
import reactor.util.function.Tuple3;
5756
import reactor.util.function.Tuples;
5857

5958
/**
@@ -134,12 +133,12 @@ public void addSymbols(Annotation node, ITypeBinding typeBinding, Collection<ITy
134133
@Override
135134
protected void addSymbolsPass1(TypeDeclaration typeDeclaration, SpringIndexerJavaContext context, TextDocument doc) {
136135
// this checks function beans that are defined as implementations of Function interfaces
137-
Tuple3<String, ITypeBinding, DocumentRegion> functionBean = FunctionUtils.getFunctionBean(typeDeclaration, doc);
136+
ITypeBinding functionBean = FunctionUtils.getFunctionBean(typeDeclaration, doc);
138137
if (functionBean != null) {
139138
try {
140-
String beanName = functionBean.getT1();
141-
ITypeBinding beanType = functionBean.getT2();
142-
Location beanLocation = new Location(doc.getUri(), doc.toRange(functionBean.getT3()));
139+
String beanName = BeanUtils.getBeanName(typeDeclaration);
140+
ITypeBinding beanType = functionBean;
141+
Location beanLocation = new Location(doc.getUri(), doc.toRange(ASTUtils.nodeRegion(doc, typeDeclaration.getName())));
143142

144143
WorkspaceSymbol symbol = new WorkspaceSymbol(
145144
beanLabel(true, beanName, beanType.getName(), null),

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

+49-15
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@
1818
import java.util.stream.Stream;
1919

2020
import org.eclipse.jdt.core.dom.Annotation;
21+
import org.eclipse.jdt.core.dom.IMethodBinding;
2122
import org.eclipse.jdt.core.dom.ITypeBinding;
23+
import org.eclipse.jdt.core.dom.MethodDeclaration;
2224
import org.eclipse.jdt.core.dom.TypeDeclaration;
2325
import org.eclipse.lsp4j.Location;
2426
import org.eclipse.lsp4j.SymbolKind;
2527
import org.eclipse.lsp4j.WorkspaceSymbol;
2628
import org.eclipse.lsp4j.jsonrpc.messages.Either;
27-
import org.eclipse.lsp4j.jsonrpc.messages.Tuple;
28-
import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two;
2929
import org.slf4j.Logger;
3030
import org.slf4j.LoggerFactory;
3131
import org.springframework.ide.vscode.boot.java.Annotations;
32+
import org.springframework.ide.vscode.boot.java.events.EventListenerIndexElement;
3233
import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider;
3334
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
3435
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
@@ -39,6 +40,7 @@
3940
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
4041
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
4142
import org.springframework.ide.vscode.commons.util.BadLocationException;
43+
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
4244
import org.springframework.ide.vscode.commons.util.text.TextDocument;
4345

4446
/**
@@ -53,12 +55,7 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider {
5355
protected void addSymbolsPass1(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
5456
try {
5557
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
56-
Two<EnhancedSymbolInformation, Bean> result = createSymbol(node, annotationType, metaAnnotations, doc);
57-
58-
EnhancedSymbolInformation enhancedSymbol = result.getFirst();
59-
Bean beanDefinition = result.getSecond();
60-
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol));
61-
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
58+
createSymbol(node, annotationType, metaAnnotations, context, doc);
6259
}
6360
else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) {
6461
WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc);
@@ -71,7 +68,7 @@ else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName(
7168
}
7269
}
7370

74-
protected Tuple.Two<EnhancedSymbolInformation, Bean> createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, TextDocument doc) throws BadLocationException {
71+
protected void createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) throws BadLocationException {
7572
String annotationTypeName = annotationType.getName();
7673

7774
Collection<String> metaAnnotationNames = metaAnnotations.stream()
@@ -81,7 +78,7 @@ protected Tuple.Two<EnhancedSymbolInformation, Bean> createSymbol(Annotation nod
8178
TypeDeclaration type = (TypeDeclaration) node.getParent();
8279

8380
String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, type);
84-
ITypeBinding beanType = getBeanType(type);
81+
ITypeBinding beanType = type.resolveBinding();
8582

8683
Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()));
8784

@@ -107,8 +104,49 @@ protected Tuple.Two<EnhancedSymbolInformation, Bean> createSymbol(Annotation nod
107104
.toArray(AnnotationMetadata[]::new);
108105

109106
Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration);
107+
108+
// event listener - create child element, if necessary
109+
ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER));
110+
if (inTypeHierarchy != null) {
111+
112+
MethodDeclaration handleEventMethod = findHandleEventMethod(type);
113+
if (handleEventMethod != null) {
114+
115+
IMethodBinding methodBinding = handleEventMethod.resolveBinding();
116+
ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
117+
if (parameterTypes != null && parameterTypes.length == 1) {
118+
119+
ITypeBinding eventType = parameterTypes[0];
120+
String eventTypeFq = eventType.getQualifiedName();
121+
122+
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName());
123+
Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange());
124+
125+
Collection<Annotation> annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod);
126+
AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc);
127+
128+
EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, handleEventMethodAnnotations);
129+
beanDefinition.addChild(eventElement);
130+
}
131+
}
132+
}
133+
134+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), new EnhancedSymbolInformation(symbol)));
135+
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
136+
}
110137

111-
return Tuple.two(new EnhancedSymbolInformation(symbol), beanDefinition);
138+
private MethodDeclaration findHandleEventMethod(TypeDeclaration type) {
139+
MethodDeclaration[] methods = type.getMethods();
140+
141+
for (MethodDeclaration method : methods) {
142+
IMethodBinding binding = method.resolveBinding();
143+
String name = binding.getName();
144+
145+
if (name != null && name.equals("onApplicationEvent")) {
146+
return method;
147+
}
148+
}
149+
return null;
112150
}
113151

114152
protected String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
@@ -137,9 +175,5 @@ protected String beanLabel(String searchPrefix, String annotationTypeName, Colle
137175
symbolLabel.append(beanType);
138176
return symbolLabel.toString();
139177
}
140-
141-
private ITypeBinding getBeanType(TypeDeclaration type) {
142-
return type.resolveBinding();
143-
}
144178

145179
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.events;
12+
13+
import org.eclipse.lsp4j.Location;
14+
import org.springframework.ide.vscode.commons.protocol.spring.AbstractSpringIndexElement;
15+
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
16+
17+
/**
18+
* @author Martin Lippert
19+
*/
20+
public class EventListenerIndexElement extends AbstractSpringIndexElement {
21+
22+
private final String eventType;
23+
private final Location location;
24+
private final AnnotationMetadata[] annotations;
25+
26+
public EventListenerIndexElement(String eventType, Location location, AnnotationMetadata[] annotations) {
27+
this.eventType = eventType;
28+
this.location = location;
29+
this.annotations = annotations;
30+
}
31+
32+
public String getEventType() {
33+
return eventType;
34+
}
35+
36+
public AnnotationMetadata[] getAnnotations() {
37+
return annotations;
38+
}
39+
40+
public Location getLocation() {
41+
return location;
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.events;
12+
import java.util.Collection;
13+
import java.util.List;
14+
15+
import org.eclipse.jdt.core.dom.ASTNode;
16+
import org.eclipse.jdt.core.dom.Annotation;
17+
import org.eclipse.jdt.core.dom.ITypeBinding;
18+
import org.eclipse.jdt.core.dom.IVariableBinding;
19+
import org.eclipse.jdt.core.dom.MethodDeclaration;
20+
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
21+
import org.eclipse.lsp4j.Location;
22+
import org.eclipse.lsp4j.SymbolKind;
23+
import org.eclipse.lsp4j.WorkspaceSymbol;
24+
import org.eclipse.lsp4j.jsonrpc.messages.Either;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
import org.springframework.ide.vscode.boot.java.beans.CachedBean;
28+
import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider;
29+
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
30+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
31+
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
32+
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
33+
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
34+
import org.springframework.ide.vscode.commons.util.BadLocationException;
35+
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
36+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
37+
38+
/**
39+
* @author Martin Lippert
40+
*/
41+
public class EventListenerSymbolProvider extends AbstractSymbolProvider {
42+
43+
private static final Logger log = LoggerFactory.getLogger(EventListenerSymbolProvider.class);
44+
45+
@Override
46+
protected void addSymbolsPass1(Annotation node, ITypeBinding typeBinding, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
47+
if (node == null) return;
48+
49+
ASTNode parent = node.getParent();
50+
if (parent == null || !(parent instanceof MethodDeclaration)) return;
51+
52+
MethodDeclaration method = (MethodDeclaration) parent;
53+
54+
55+
// symbol
56+
try {
57+
String symbolLabel = createEventListenerSymbolLabel(node, method);
58+
WorkspaceSymbol symbol = new WorkspaceSymbol(symbolLabel, SymbolKind.Interface,
59+
Either.forLeft(new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))));
60+
61+
EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol);
62+
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol));
63+
64+
65+
// index element for event listener
66+
Collection<Annotation> annotationsOnMethod = ASTUtils.getAnnotations(method);
67+
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);
68+
69+
List<CachedBean> beans = context.getBeans();
70+
if (beans.size() > 0 ) {
71+
72+
CachedBean cachedBean = beans.get(beans.size() - 1);
73+
if (cachedBean.getDocURI().equals(doc.getUri())) {
74+
75+
ITypeBinding eventType = getEventType(node, method);
76+
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, method.getName());
77+
Location location = new Location(doc.getUri(), nodeRegion.asRange());
78+
79+
cachedBean.getBean().addChild(new EventListenerIndexElement(eventType != null ? eventType.getQualifiedName() : "", location, annotations));
80+
}
81+
}
82+
} catch (BadLocationException e) {
83+
log.error("", e);
84+
}
85+
}
86+
87+
private String createEventListenerSymbolLabel(Annotation node, MethodDeclaration method) {
88+
// event listener annotation type
89+
String annotationTypeName = getAnnotationTypeName(node);
90+
ITypeBinding eventType = getEventType(node, method);
91+
92+
if (annotationTypeName != null) {
93+
return "@" + annotationTypeName + (eventType != null ? " (" + eventType.getName() + ")" : "");
94+
}
95+
else {
96+
return node.toString();
97+
}
98+
}
99+
100+
private ITypeBinding getEventType(Annotation node, MethodDeclaration method) {
101+
List<?> parameters = method.parameters();
102+
if (parameters != null && parameters.size() == 1) {
103+
SingleVariableDeclaration param = (SingleVariableDeclaration) parameters.get(0);
104+
105+
IVariableBinding paramBinding = param.resolveBinding();
106+
if (paramBinding != null) {
107+
ITypeBinding paramType = paramBinding.getType();
108+
return paramType != null ? paramType : null;
109+
}
110+
}
111+
112+
return null;
113+
}
114+
115+
private String getAnnotationTypeName(Annotation node) {
116+
ITypeBinding typeBinding = node.resolveTypeBinding();
117+
if (typeBinding != null) {
118+
return typeBinding.getName();
119+
}
120+
else {
121+
return null;
122+
}
123+
}
124+
125+
126+
}

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ public final class AnnotationParamSpelExtractor {
2828
private static final String SPRING_CACHEABLE = "org.springframework.cache.annotation.Cacheable";
2929
private static final String SPRING_CACHE_EVICT = "org.springframework.cache.annotation.CacheEvict";
3030

31-
private static final String SPRING_EVENT_LISTENER = "org.springframework.context.event.EventListener";
32-
3331
private static final String SPRING_PRE_AUTHORIZE = "org.springframework.security.access.prepost.PreAuthorize";
3432
private static final String SPRING_PRE_FILTER = "org.springframework.security.access.prepost.PreFilter";
3533
private static final String SPRING_POST_AUTHORIZE = "org.springframework.security.access.prepost.PostAuthorize";
@@ -48,7 +46,7 @@ public final class AnnotationParamSpelExtractor {
4846
new AnnotationParamSpelExtractor(SPRING_CACHE_EVICT, "key", "", ""),
4947
new AnnotationParamSpelExtractor(SPRING_CACHE_EVICT, "condition", "", ""),
5048

51-
new AnnotationParamSpelExtractor(SPRING_EVENT_LISTENER, "condition", "", ""),
49+
new AnnotationParamSpelExtractor(Annotations.EVENT_LISTENER, "condition", "", ""),
5250

5351
new AnnotationParamSpelExtractor(SPRING_PRE_AUTHORIZE, null, "", ""),
5452
new AnnotationParamSpelExtractor(SPRING_PRE_AUTHORIZE, "value", "", ""),

0 commit comments

Comments
 (0)