Skip to content

Commit

Permalink
GH-1348: first steps towards indexing event listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
martinlippert committed Jan 30, 2025
1 parent 23eff6d commit a835804
Show file tree
Hide file tree
Showing 24 changed files with 1,042 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider;
import org.springframework.ide.vscode.boot.java.beans.FeignClientSymbolProvider;
import org.springframework.ide.vscode.boot.java.data.DataRepositorySymbolProvider;
import org.springframework.ide.vscode.boot.java.events.EventListenerSymbolProvider;
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider;
import org.springframework.ide.vscode.boot.java.utils.RestrictedDefaultSymbolProvider;
Expand All @@ -35,6 +36,7 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache)
ComponentSymbolProvider componentSymbolProvider = new ComponentSymbolProvider();
RestrictedDefaultSymbolProvider restrictedDefaultSymbolProvider = new RestrictedDefaultSymbolProvider();
DataRepositorySymbolProvider dataRepositorySymbolProvider = new DataRepositorySymbolProvider();
EventListenerSymbolProvider eventListenerSymbolProvider = new EventListenerSymbolProvider();

providers.put(Annotations.SPRING_REQUEST_MAPPING, requestMappingSymbolProvider);
providers.put(Annotations.SPRING_GET_MAPPING, requestMappingSymbolProvider);
Expand Down Expand Up @@ -70,6 +72,7 @@ AnnotationHierarchyAwareLookup<SymbolProvider> symbolProviders(IndexCache cache)
providers.put(Annotations.CONDITIONAL_ON_SINGLE_CANDIDATE, restrictedDefaultSymbolProvider);

providers.put(Annotations.REPOSITORY, dataRepositorySymbolProvider);
providers.put(Annotations.EVENT_LISTENER, eventListenerSymbolProvider);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public class Annotations {
public static final Set<String> NAMED_ANNOTATIONS = Set.of(Annotations.NAMED_JAKARTA, Annotations.NAMED_JAVAX);

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 Map<String, String> AOP_ANNOTATIONS = Map.of(
"org.aspectj.lang.annotation.Pointcut", "Pointcut",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,10 @@ public static String getBeanNameFromType(String typeName) {
}
return typeName;
}

public static String getBeanName(TypeDeclaration typeDeclaration) {
String beanName = typeDeclaration.getName().toString();
return BeanUtils.getBeanNameFromType(beanName);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
import com.google.common.collect.ImmutableList;

import reactor.util.function.Tuple2;
import reactor.util.function.Tuple3;
import reactor.util.function.Tuples;

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

WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel(true, beanName, beanType.getName(), null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@
import java.util.stream.Stream;

import org.eclipse.jdt.core.dom.Annotation;
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.TypeDeclaration;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Tuple;
import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two;
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.handlers.AbstractSymbolProvider;
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
Expand All @@ -39,6 +40,7 @@
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;

/**
Expand All @@ -53,12 +55,7 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider {
protected void addSymbolsPass1(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
try {
if (node != null && node.getParent() != null && node.getParent() instanceof TypeDeclaration) {
Two<EnhancedSymbolInformation, Bean> result = createSymbol(node, annotationType, metaAnnotations, doc);

EnhancedSymbolInformation enhancedSymbol = result.getFirst();
Bean beanDefinition = result.getSecond();
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
createSymbol(node, annotationType, metaAnnotations, context, doc);
}
else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) {
WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc);
Expand All @@ -71,7 +68,7 @@ else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName(
}
}

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

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

String beanName = BeanUtils.getBeanNameFromComponentAnnotation(node, type);
ITypeBinding beanType = getBeanType(type);
ITypeBinding beanType = type.resolveBinding();

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

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

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, handleEventMethodAnnotations);
beanDefinition.addChild(eventElement);
}
}
}

context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), new EnhancedSymbolInformation(symbol)));
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
}

return Tuple.two(new EnhancedSymbolInformation(symbol), beanDefinition);
private MethodDeclaration findHandleEventMethod(TypeDeclaration type) {
MethodDeclaration[] methods = type.getMethods();

for (MethodDeclaration method : methods) {
IMethodBinding binding = method.resolveBinding();
String name = binding.getName();

if (name != null && name.equals("onApplicationEvent")) {
return method;
}
}
return null;
}

protected String beanLabel(String searchPrefix, String annotationTypeName, Collection<String> metaAnnotationNames, String beanName, String beanType) {
Expand Down Expand Up @@ -137,9 +175,5 @@ protected String beanLabel(String searchPrefix, String annotationTypeName, Colle
symbolLabel.append(beanType);
return symbolLabel.toString();
}

private ITypeBinding getBeanType(TypeDeclaration type) {
return type.resolveBinding();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*******************************************************************************
* 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;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;

/**
* @author Martin Lippert
*/
public class EventListenerIndexElement extends AbstractSpringIndexElement {

private final String eventType;
private final Location location;
private final AnnotationMetadata[] annotations;

public EventListenerIndexElement(String eventType, Location location, AnnotationMetadata[] annotations) {
this.eventType = eventType;
this.location = location;
this.annotations = annotations;
}

public String getEventType() {
return eventType;
}

public AnnotationMetadata[] getAnnotations() {
return annotations;
}

public Location getLocation() {
return location;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*******************************************************************************
* 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 java.util.Collection;
import java.util.List;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.SymbolKind;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.beans.CachedBean;
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;
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.DocumentRegion;
import org.springframework.ide.vscode.commons.util.text.TextDocument;

/**
* @author Martin Lippert
*/
public class EventListenerSymbolProvider extends AbstractSymbolProvider {

private static final Logger log = LoggerFactory.getLogger(EventListenerSymbolProvider.class);

@Override
protected void addSymbolsPass1(Annotation node, ITypeBinding typeBinding, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
if (node == null) return;

ASTNode parent = node.getParent();
if (parent == null || !(parent instanceof MethodDeclaration)) return;

MethodDeclaration method = (MethodDeclaration) parent;


// symbol
try {
String symbolLabel = createEventListenerSymbolLabel(node, method);
WorkspaceSymbol symbol = new WorkspaceSymbol(symbolLabel, SymbolKind.Interface,
Either.forLeft(new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))));

EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol);
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol));


// index element for event listener
Collection<Annotation> annotationsOnMethod = ASTUtils.getAnnotations(method);
AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc);

List<CachedBean> beans = context.getBeans();
if (beans.size() > 0 ) {

CachedBean cachedBean = beans.get(beans.size() - 1);
if (cachedBean.getDocURI().equals(doc.getUri())) {

ITypeBinding eventType = getEventType(node, method);
DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, method.getName());
Location location = new Location(doc.getUri(), nodeRegion.asRange());

cachedBean.getBean().addChild(new EventListenerIndexElement(eventType != null ? eventType.getQualifiedName() : "", location, annotations));
}
}
} catch (BadLocationException e) {
log.error("", e);
}
}

private String createEventListenerSymbolLabel(Annotation node, MethodDeclaration method) {
// event listener annotation type
String annotationTypeName = getAnnotationTypeName(node);
ITypeBinding eventType = getEventType(node, method);

if (annotationTypeName != null) {
return "@" + annotationTypeName + (eventType != null ? " (" + eventType.getName() + ")" : "");
}
else {
return node.toString();
}
}

private ITypeBinding getEventType(Annotation node, MethodDeclaration method) {
List<?> parameters = method.parameters();
if (parameters != null && parameters.size() == 1) {
SingleVariableDeclaration param = (SingleVariableDeclaration) parameters.get(0);

IVariableBinding paramBinding = param.resolveBinding();
if (paramBinding != null) {
ITypeBinding paramType = paramBinding.getType();
return paramType != null ? paramType : null;
}
}

return null;
}

private String getAnnotationTypeName(Annotation node) {
ITypeBinding typeBinding = node.resolveTypeBinding();
if (typeBinding != null) {
return typeBinding.getName();
}
else {
return null;
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ public final class AnnotationParamSpelExtractor {
private static final String SPRING_CACHEABLE = "org.springframework.cache.annotation.Cacheable";
private static final String SPRING_CACHE_EVICT = "org.springframework.cache.annotation.CacheEvict";

private static final String SPRING_EVENT_LISTENER = "org.springframework.context.event.EventListener";

private static final String SPRING_PRE_AUTHORIZE = "org.springframework.security.access.prepost.PreAuthorize";
private static final String SPRING_PRE_FILTER = "org.springframework.security.access.prepost.PreFilter";
private static final String SPRING_POST_AUTHORIZE = "org.springframework.security.access.prepost.PostAuthorize";
Expand All @@ -48,7 +46,7 @@ public final class AnnotationParamSpelExtractor {
new AnnotationParamSpelExtractor(SPRING_CACHE_EVICT, "key", "", ""),
new AnnotationParamSpelExtractor(SPRING_CACHE_EVICT, "condition", "", ""),

new AnnotationParamSpelExtractor(SPRING_EVENT_LISTENER, "condition", "", ""),
new AnnotationParamSpelExtractor(Annotations.EVENT_LISTENER, "condition", "", ""),

new AnnotationParamSpelExtractor(SPRING_PRE_AUTHORIZE, null, "", ""),
new AnnotationParamSpelExtractor(SPRING_PRE_AUTHORIZE, "value", "", ""),
Expand Down
Loading

0 comments on commit a835804

Please sign in to comment.