Skip to content

Commit 84f549c

Browse files
arik-digasafchen-dig
authored andcommitted
Feature/SpringBoot discovery of HTTP endpoints (#426)
1 parent 528e147 commit 84f549c

File tree

9 files changed

+249
-19
lines changed

9 files changed

+249
-19
lines changed

java/src/main/java/org/digma/intellij/plugin/idea/psi/java/GrpcFramework.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import java.util.Collection;
2121
import java.util.Objects;
2222

23-
public class GrpcFramework {
23+
public class GrpcFramework implements IEndpointDiscovery {
2424
private static final Logger LOGGER = Logger.getInstance(GrpcFramework.class);
2525

2626
public static final String BINDABLE_SERVICE_ANNOTATION_STR = "io.grpc.BindableService";
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.digma.intellij.plugin.idea.psi.java;
2+
3+
import com.intellij.psi.PsiFile;
4+
import org.digma.intellij.plugin.model.discovery.DocumentInfo;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
public interface IEndpointDiscovery {
8+
void endpointDiscovery(@NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo);
9+
}

java/src/main/java/org/digma/intellij/plugin/idea/psi/java/JavaCodeObjectDiscovery.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import com.intellij.openapi.diagnostic.Logger;
44
import com.intellij.openapi.project.Project;
5-
import com.intellij.psi.*;
5+
import com.intellij.psi.JavaPsiFacade;
6+
import com.intellij.psi.PsiClass;
7+
import com.intellij.psi.PsiFile;
8+
import com.intellij.psi.PsiJavaFile;
9+
import com.intellij.psi.PsiMethod;
10+
import com.intellij.psi.PsiReference;
611
import com.intellij.psi.search.GlobalSearchScope;
712
import com.intellij.psi.search.searches.AnnotatedElementsSearch;
813
import com.intellij.psi.search.searches.MethodReferencesSearch;
@@ -15,7 +20,11 @@
1520
import org.digma.intellij.plugin.psi.PsiUtils;
1621
import org.jetbrains.annotations.NotNull;
1722

18-
import java.util.*;
23+
import java.util.ArrayList;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Objects;
1928
import java.util.concurrent.TimeUnit;
2029

2130
import static org.digma.intellij.plugin.idea.psi.java.Constants.SPAN_BUILDER_FQN;
@@ -29,7 +38,7 @@ public class JavaCodeObjectDiscovery {
2938
private static final Logger LOGGER = Logger.getInstance(JavaCodeObjectDiscovery.class);
3039

3140

32-
public static @NotNull DocumentInfo buildDocumentInfo(@NotNull Project project, @NotNull PsiJavaFile psiJavaFile, MicronautFramework micronautFramework, JaxrsFramework jaxrsFramework, GrpcFramework grpcFramework) {
41+
public static @NotNull DocumentInfo buildDocumentInfo(@NotNull Project project, @NotNull PsiJavaFile psiJavaFile, @NotNull List<IEndpointDiscovery> endpointDiscoveryList) {
3342
var stopWatch = StopWatch.createStarted();
3443

3544
try {
@@ -47,7 +56,7 @@ public class JavaCodeObjectDiscovery {
4756
buildDocumentInfo is always called in smart mode and so span discovery and endpoint discovery should work.
4857
4958
*/
50-
enrichDocumentInfo(project,documentInfo,psiJavaFile,micronautFramework,jaxrsFramework,grpcFramework);
59+
enrichDocumentInfo(project, documentInfo, psiJavaFile, endpointDiscoveryList);
5160
return documentInfo;
5261

5362
} finally {
@@ -106,7 +115,7 @@ private static void collectMethods(@NotNull String fileUri, @NotNull PsiClass[]
106115
}
107116

108117

109-
private static void enrichDocumentInfo(Project project, @NotNull DocumentInfo documentInfo, @NotNull PsiFile psiFile, @NotNull MicronautFramework micronautFramework, @NotNull JaxrsFramework jaxrsFramework, @NotNull GrpcFramework grpcFramework) {
118+
private static void enrichDocumentInfo(Project project, @NotNull DocumentInfo documentInfo, @NotNull PsiFile psiFile, @NotNull List<IEndpointDiscovery> endpointDiscoveryList) {
110119
/*
111120
need to make sure that spans and endpoints are cleared here.
112121
why?
@@ -120,7 +129,7 @@ private static void enrichDocumentInfo(Project project, @NotNull DocumentInfo do
120129
});
121130

122131
spanDiscovery(project, psiFile, documentInfo);
123-
endpointDiscovery(psiFile, documentInfo, micronautFramework, jaxrsFramework, grpcFramework);
132+
endpointDiscovery(psiFile, documentInfo, endpointDiscoveryList);
124133
}
125134

126135
private static void spanDiscovery(@NotNull Project project, @NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo) {
@@ -129,11 +138,9 @@ private static void spanDiscovery(@NotNull Project project, @NotNull PsiFile psi
129138
startSpanMethodCallSpanDiscovery(project, psiFile, documentInfo);
130139
}
131140

132-
private static void endpointDiscovery(@NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo, @NotNull MicronautFramework micronautFramework, @NotNull JaxrsFramework jaxrsFramework, @NotNull GrpcFramework grpcFramework) {
141+
private static void endpointDiscovery(@NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo, @NotNull List<IEndpointDiscovery> endpointDiscoveryList) {
133142
Log.log(LOGGER::debug, "Building endpoints for file {}", psiFile);
134-
micronautFramework.endpointDiscovery(psiFile, documentInfo);
135-
jaxrsFramework.endpointDiscovery(psiFile, documentInfo);
136-
grpcFramework.endpointDiscovery(psiFile, documentInfo);
143+
endpointDiscoveryList.forEach(it -> it.endpointDiscovery(psiFile, documentInfo));
137144
}
138145

139146

java/src/main/java/org/digma/intellij/plugin/idea/psi/java/JavaLanguageService.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public class JavaLanguageService implements LanguageService {
4444
private final MicronautFramework micronautFramework;
4545
private final JaxrsFramework jaxrsFramework;
4646
private final GrpcFramework grpcFramework;
47-
47+
private final SpringBootFramework springBootFramework;
48+
private List<IEndpointDiscovery> endpointDiscoveryList;
4849

4950

5051
/*
@@ -57,11 +58,11 @@ public JavaLanguageService(Project project) {
5758
this.micronautFramework = new MicronautFramework(project);
5859
this.jaxrsFramework = new JaxrsFramework(project);
5960
this.grpcFramework = new GrpcFramework(project);
60-
61+
this.springBootFramework = new SpringBootFramework(project);
62+
endpointDiscoveryList = List.of(micronautFramework, jaxrsFramework, grpcFramework, springBootFramework);
6163
}
6264

6365

64-
6566
@Override
6667
public void ensureStartupOnEDT(@NotNull Project project) {
6768
//nothing to do
@@ -319,7 +320,7 @@ public void environmentChanged(String newEnv) {
319320
Log.log(LOGGER::debug, "got buildDocumentInfo request for {}", psiFile);
320321
//must be PsiJavaFile , this method should be called only for java files
321322
if (psiFile instanceof PsiJavaFile psiJavaFile) {
322-
return JavaCodeObjectDiscovery.buildDocumentInfo(project, psiJavaFile, micronautFramework, jaxrsFramework, grpcFramework);
323+
return JavaCodeObjectDiscovery.buildDocumentInfo(project, psiJavaFile, endpointDiscoveryList);
323324
}else{
324325
Log.log(LOGGER::debug, "psi file is noy java, returning empty DocumentInfo for {}", psiFile);
325326
return new DocumentInfo(PsiUtils.psiFileToUri(psiFile), new HashMap<>());

java/src/main/java/org/digma/intellij/plugin/idea/psi/java/JavaLanguageUtils.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
package org.digma.intellij.plugin.idea.psi.java;
22

33
import com.intellij.lang.jvm.JvmMethod;
4-
import com.intellij.psi.*;
4+
import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute;
5+
import com.intellij.psi.PsiAnnotation;
6+
import com.intellij.psi.PsiClass;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.PsiExpression;
9+
import com.intellij.psi.PsiExpressionList;
10+
import com.intellij.psi.PsiField;
11+
import com.intellij.psi.PsiJavaFile;
12+
import com.intellij.psi.PsiLiteral;
13+
import com.intellij.psi.PsiLiteralExpression;
14+
import com.intellij.psi.PsiLocalVariable;
15+
import com.intellij.psi.PsiMethod;
16+
import com.intellij.psi.PsiReferenceExpression;
517
import com.intellij.psi.impl.source.PsiClassReferenceType;
618
import org.jetbrains.annotations.NotNull;
719
import org.jetbrains.annotations.Nullable;
820

21+
import java.util.List;
922
import java.util.function.Predicate;
1023

1124

@@ -132,7 +145,7 @@ public static String getPsiFieldValue(@NotNull PsiField psiField) {
132145
}else if (initializer instanceof PsiReferenceExpression) {
133146
return getPsiReferenceExpressionValue((PsiReferenceExpression) initializer);
134147
}
135-
return null;
148+
return psiField.getText();
136149
}
137150

138151

@@ -148,7 +161,18 @@ public static PsiMethod findMethodInClass(@NotNull PsiClass psiClass, @NotNull S
148161
}
149162

150163

164+
@Nullable
165+
public static String getValueOfFirstMatchingAnnotationAttribute(@NotNull PsiAnnotation psiAnnotation, @NotNull List<String> attributeList, @Nullable String defaultValue) {
166+
List<JvmAnnotationAttribute> annotationAttributes = psiAnnotation.getAttributes();
151167

168+
for (JvmAnnotationAttribute curr : annotationAttributes) {
169+
if (attributeList.contains(curr.getAttributeName())) {
170+
String theValue = JavaLanguageUtils.getPsiAnnotationAttributeValue(psiAnnotation, curr.getAttributeName());
171+
return theValue;
172+
}
173+
}
174+
return defaultValue;
175+
}
152176

153177
/**
154178
* test if the psiElement is a method with methodName and containingClassName.

java/src/main/java/org/digma/intellij/plugin/idea/psi/java/JaxrsFramework.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.Objects;
2020
import java.util.stream.Collectors;
2121

22-
public class JaxrsFramework {
22+
public class JaxrsFramework implements IEndpointDiscovery {
2323

2424
private static final Logger LOGGER = Logger.getInstance(JaxrsFramework.class);
2525
private static final String JAX_RS_PATH_ANNOTATION_STR = "javax.ws.rs.Path";

java/src/main/java/org/digma/intellij/plugin/idea/psi/java/MicronautFramework.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.util.Objects;
2323
import java.util.stream.Collectors;
2424

25-
public class MicronautFramework {
25+
public class MicronautFramework implements IEndpointDiscovery {
2626

2727
private static final Logger LOGGER = Logger.getInstance(MicronautFramework.class);
2828
private static final String CONTROLLER_ANNOTATION_STR = "io.micronaut.http.annotation.Controller";
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package org.digma.intellij.plugin.idea.psi.java;
2+
3+
import com.intellij.openapi.diagnostic.Logger;
4+
import com.intellij.openapi.project.Project;
5+
import com.intellij.psi.JavaPsiFacade;
6+
import com.intellij.psi.PsiAnnotation;
7+
import com.intellij.psi.PsiClass;
8+
import com.intellij.psi.PsiFile;
9+
import com.intellij.psi.PsiMethod;
10+
import com.intellij.psi.search.GlobalSearchScope;
11+
import com.intellij.psi.search.searches.AnnotatedElementsSearch;
12+
import com.intellij.util.Query;
13+
import org.digma.intellij.plugin.log.Log;
14+
import org.digma.intellij.plugin.model.discovery.DocumentInfo;
15+
import org.digma.intellij.plugin.model.discovery.EndpointInfo;
16+
import org.digma.intellij.plugin.model.discovery.MethodInfo;
17+
import org.jetbrains.annotations.NotNull;
18+
import org.jetbrains.annotations.Nullable;
19+
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Objects;
23+
import java.util.stream.Collectors;
24+
25+
public class SpringBootFramework implements IEndpointDiscovery {
26+
27+
private static final Logger LOGGER = Logger.getInstance(SpringBootFramework.class);
28+
private static final String CONTROLLER_ANNOTATION_STR = "org.springframework.stereotype.Controller";
29+
private static final String REST_CONTROLLER_ANNOTATION_STR = "org.springframework.web.bind.annotation.RestController";
30+
private static final String HTTP_DELETE_ANNOTATION_STR = "org.springframework.web.bind.annotation.DeleteMapping";
31+
private static final String HTTP_GET_ANNOTATION_STR = "org.springframework.web.bind.annotation.GetMapping";
32+
private static final String HTTP_PATCH_ANNOTATION_STR = "org.springframework.web.bind.annotation.PatchMapping";
33+
private static final String HTTP_POST_ANNOTATION_STR = "org.springframework.web.bind.annotation.PostMapping";
34+
private static final String HTTP_PUT_ANNOTATION_STR = "org.springframework.web.bind.annotation.PutMapping";
35+
private static final String HTTP_REQUEST_MAPPING_ANNOTATION_STR = "org.springframework.web.bind.annotation.RequestMapping";
36+
private static final List<String> HTTP_METHODS_ANNOTATION_STR_LIST = List.of(
37+
HTTP_DELETE_ANNOTATION_STR, HTTP_GET_ANNOTATION_STR, HTTP_PATCH_ANNOTATION_STR, HTTP_POST_ANNOTATION_STR, HTTP_PUT_ANNOTATION_STR, HTTP_REQUEST_MAPPING_ANNOTATION_STR);
38+
39+
// related to RequestMapping, one of those attributes should be filled
40+
private static final List<String> ATTRIBUTES_OF_PATH = List.of("value", "path");
41+
//
42+
private static final Map<String, String> MAP_HTTP_XXX_ANNOT_2_HTTP_METHOD;
43+
44+
static {
45+
MAP_HTTP_XXX_ANNOT_2_HTTP_METHOD = Map.of(
46+
HTTP_DELETE_ANNOTATION_STR, "DELETE",
47+
HTTP_GET_ANNOTATION_STR, "GET",
48+
HTTP_PATCH_ANNOTATION_STR, "PATCH",
49+
HTTP_POST_ANNOTATION_STR, "POST",
50+
HTTP_PUT_ANNOTATION_STR, "PUT"
51+
);
52+
}
53+
54+
private final Project project;
55+
56+
// late init
57+
private boolean lateInitAlready = false;
58+
private PsiClass controllerAnnotationClass;
59+
private List<JavaAnnotation> httpMethodsAnnotations;
60+
61+
public SpringBootFramework(Project project) {
62+
this.project = project;
63+
}
64+
65+
private void lateInit() {
66+
if (lateInitAlready) return;
67+
68+
JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
69+
controllerAnnotationClass = psiFacade.findClass(CONTROLLER_ANNOTATION_STR, GlobalSearchScope.allScope(project));
70+
initHttpMethodAnnotations(psiFacade);
71+
72+
lateInitAlready = true;
73+
}
74+
75+
private void initHttpMethodAnnotations(JavaPsiFacade psiFacade) {
76+
httpMethodsAnnotations = HTTP_METHODS_ANNOTATION_STR_LIST.stream()
77+
.map(currFqn -> {
78+
PsiClass psiClass = psiFacade.findClass(currFqn, GlobalSearchScope.allScope(project));
79+
if (psiClass == null) return null;
80+
return new JavaAnnotation(currFqn, psiClass);
81+
})
82+
.filter(Objects::nonNull)
83+
.collect(Collectors.toUnmodifiableList());
84+
}
85+
86+
private boolean isSpringBootWebRelevant() {
87+
return controllerAnnotationClass != null;
88+
}
89+
90+
public void endpointDiscovery(@NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo) {
91+
lateInit();
92+
if (!isSpringBootWebRelevant()) {
93+
return;
94+
}
95+
96+
httpMethodsAnnotations.forEach(currAnnotation -> {
97+
Query<PsiMethod> psiMethodsInFile = AnnotatedElementsSearch.searchPsiMethods(currAnnotation.getPsiClass(), GlobalSearchScope.fileScope(psiFile));
98+
99+
for (PsiMethod currPsiMethod : psiMethodsInFile) {
100+
final PsiAnnotation mappingPsiAnnotationOnMethod = currPsiMethod.getAnnotation(currAnnotation.getClassNameFqn());
101+
if (mappingPsiAnnotationOnMethod == null) {
102+
continue; // very unlikely
103+
}
104+
105+
final PsiClass controllerClass = currPsiMethod.getContainingClass();
106+
if (controllerClass == null) {
107+
continue; // very unlikely
108+
}
109+
110+
if (!JavaPsiUtils.hasOneOfAnnotations(controllerClass, CONTROLLER_ANNOTATION_STR, REST_CONTROLLER_ANNOTATION_STR)) {
111+
continue; // skip this method, since its class is not a controller (or rest controller)
112+
}
113+
114+
final PsiAnnotation controllerReqMappingAnnotation = controllerClass.getAnnotation(HTTP_REQUEST_MAPPING_ANNOTATION_STR);
115+
String endpointUriPrefix = "";
116+
if (controllerReqMappingAnnotation != null) {
117+
endpointUriPrefix = JavaLanguageUtils.getValueOfFirstMatchingAnnotationAttribute(controllerReqMappingAnnotation, ATTRIBUTES_OF_PATH, "");
118+
}
119+
final String endpointUriSuffix = JavaLanguageUtils.getValueOfFirstMatchingAnnotationAttribute(mappingPsiAnnotationOnMethod, ATTRIBUTES_OF_PATH, "");
120+
121+
String httpMethodName = evalHttpMethod(mappingPsiAnnotationOnMethod, controllerReqMappingAnnotation);
122+
if (httpMethodName == null) {
123+
continue; // not likely
124+
}
125+
126+
String httpEndpointCodeObjectId = createHttpEndpointCodeObjectId(httpMethodName, endpointUriPrefix, endpointUriSuffix);
127+
128+
String methodCodeObjectId = JavaLanguageUtils.createJavaMethodCodeObjectId(currPsiMethod);
129+
EndpointInfo endpointInfo = new EndpointInfo(httpEndpointCodeObjectId, methodCodeObjectId, documentInfo.getFileUri());
130+
Log.log(LOGGER::debug, "Found endpoint info '{}' for method '{}'", endpointInfo.getId(), endpointInfo.getContainingMethodId());
131+
132+
MethodInfo methodInfo = documentInfo.getMethods().get(endpointInfo.getContainingMethodId());
133+
//this method must exist in the document info
134+
Objects.requireNonNull(methodInfo, "method info " + endpointInfo.getContainingMethodId() + " must exist in DocumentInfo for " + documentInfo.getFileUri());
135+
methodInfo.addEndpoint(endpointInfo);
136+
}
137+
});
138+
}
139+
140+
@Nullable
141+
private String evalHttpMethod(@NotNull PsiAnnotation mappingPsiAnnotationOnMethod, @Nullable PsiAnnotation controllerReqMappingAnnotation) {
142+
143+
String mappedValue = MAP_HTTP_XXX_ANNOT_2_HTTP_METHOD.get(mappingPsiAnnotationOnMethod.getQualifiedName());
144+
if (mappedValue != null) {
145+
return mappedValue;
146+
}
147+
148+
if (HTTP_REQUEST_MAPPING_ANNOTATION_STR.equals(mappingPsiAnnotationOnMethod.getQualifiedName())) {
149+
// trying for attribute "method" in on method annotation
150+
{
151+
String value = JavaLanguageUtils.getPsiAnnotationAttributeValue(mappingPsiAnnotationOnMethod, "method");
152+
if (value != null) {
153+
return value.toUpperCase();
154+
}
155+
}
156+
if (controllerReqMappingAnnotation != null) {
157+
// fallback to attribute "method" in on controller annotation
158+
String value = JavaLanguageUtils.getPsiAnnotationAttributeValue(controllerReqMappingAnnotation, "method");
159+
if (value != null) {
160+
return value.toUpperCase();
161+
}
162+
}
163+
}
164+
165+
return null;
166+
}
167+
168+
@NotNull
169+
protected static String createHttpEndpointCodeObjectId(@NotNull String httpMethod, @Nullable String endpointUriPrefix, @Nullable String endpointUriSuffix) {
170+
// value for example : 'epHTTP:HTTP GET /books/get'
171+
return "" +
172+
// digma part
173+
"epHTTP:" + "HTTP " + httpMethod.toUpperCase() + " " +
174+
// Spring Web part
175+
JavaUtils.combineUri(endpointUriPrefix, endpointUriSuffix);
176+
}
177+
178+
}

java/src/main/kotlin/org/digma/intellij/plugin/idea/psi/java/JavaPsiUtils.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,16 @@ class JavaPsiUtils {
8383
return null
8484
}
8585

86+
@JvmStatic
87+
fun hasOneOfAnnotations(psiClass: PsiClass, vararg annotationsFqn: String): Boolean {
88+
annotationsFqn.forEach {
89+
val annotObj = psiClass.getAnnotation(it)
90+
if (annotObj != null) {
91+
return true
92+
}
93+
}
94+
return false
95+
}
96+
8697
}
8798
}

0 commit comments

Comments
 (0)