Skip to content

Commit ff7b56d

Browse files
committed
Feature/support micronaut http endpoint discovery (#236)
1 parent c7b4402 commit ff7b56d

File tree

9 files changed

+321
-15
lines changed

9 files changed

+321
-15
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public interface Constants {
88
String WITH_SPAN_INST_LIBRARY_1 = "io.opentelemetry.spring-boot-autoconfigure";
99
String WITH_SPAN_INST_LIBRARY_2 = "io.opentelemetry.opentelemetry-instrumentation-annotations-1.16";
1010
String WITH_SPAN_INST_LIBRARY_3 = "io.quarkus.opentelemetry";
11+
String WITH_SPAN_INST_LIBRARY_4 = "io.micronaut.code";
1112
String SPAN_BUILDER_FQN = "io.opentelemetry.api.trace.SpanBuilder";
1213
String TRACER_FQN = "io.opentelemetry.api.trace.Tracer";
1314
}

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
import com.intellij.openapi.project.Project;
1212
import com.intellij.openapi.roots.ProjectFileIndex;
1313
import com.intellij.openapi.vfs.VirtualFile;
14-
import com.intellij.psi.*;
14+
import com.intellij.psi.JavaPsiFacade;
15+
import com.intellij.psi.PsiClass;
16+
import com.intellij.psi.PsiElement;
17+
import com.intellij.psi.PsiFile;
18+
import com.intellij.psi.PsiJavaFile;
19+
import com.intellij.psi.PsiManager;
20+
import com.intellij.psi.PsiMethod;
21+
import com.intellij.psi.PsiReference;
1522
import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
1623
import com.intellij.psi.impl.source.JavaFileElementType;
1724
import com.intellij.psi.search.GlobalSearchScope;
@@ -34,7 +41,13 @@
3441
import org.jetbrains.annotations.NotNull;
3542
import org.jetbrains.annotations.Nullable;
3643

37-
import java.util.*;
44+
import java.util.ArrayList;
45+
import java.util.Collection;
46+
import java.util.HashMap;
47+
import java.util.List;
48+
import java.util.Map;
49+
import java.util.Objects;
50+
import java.util.Optional;
3851

3952
import static org.digma.intellij.plugin.idea.psi.java.Constants.SPAN_BUILDER_FQN;
4053
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.createJavaMethodCodeObjectId;
@@ -55,11 +68,13 @@ public class JavaLanguageService implements LanguageService {
5568
private final DocumentInfoService documentInfoService;
5669

5770
private final CaretContextService caretContextService;
71+
private final MicronautFramework micronautFramework;
5872

5973
public JavaLanguageService(Project project) {
6074
this.project = project;
6175
documentInfoService = project.getService(DocumentInfoService.class);
6276
caretContextService = project.getService(CaretContextService.class);
77+
this.micronautFramework = new MicronautFramework(project);
6378
}
6479

6580

@@ -376,16 +391,21 @@ public void enrichDocumentInfo(@NotNull DocumentInfo documentInfo, @NotNull PsiF
376391
*/
377392

378393
spanDiscovery(psiFile, documentInfo);
394+
endpointDiscovery(psiFile, documentInfo);
379395
}
380396

381397

382-
383398
private void spanDiscovery(PsiFile psiFile, DocumentInfo documentInfo) {
384399
Log.log(LOGGER::debug, "Building spans for file {}", psiFile);
385400
withSpanAnnotationSpanDiscovery(psiFile, documentInfo);
386401
startSpanMethodCallSpanDiscovery(psiFile, documentInfo);
387402
}
388403

404+
private void endpointDiscovery(PsiFile psiFile, DocumentInfo documentInfo) {
405+
Log.log(LOGGER::debug, "Building endpoints for file {}", psiFile);
406+
micronautFramework.endpointDiscovery(psiFile, documentInfo);
407+
}
408+
389409

390410
private void startSpanMethodCallSpanDiscovery(@NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo) {
391411

@@ -402,7 +422,7 @@ private void startSpanMethodCallSpanDiscovery(@NotNull PsiFile psiFile, @NotNull
402422
startSpanReferences.forEach(psiReference -> {
403423
SpanInfo spanInfo = JavaSpanDiscoveryUtils.getSpanInfoFromStartSpanMethodReference(project, psiReference);
404424
if (spanInfo != null) {
405-
Log.log(LOGGER::debug, "Found span info {} for method {}",spanInfo.getId(),spanInfo.getContainingMethod());
425+
Log.log(LOGGER::debug, "Found span info {} for method {}", spanInfo.getId(), spanInfo.getContainingMethod());
406426
MethodInfo methodInfo = documentInfo.getMethods().get(spanInfo.getContainingMethod());
407427
//this method must exist in the document info
408428
Objects.requireNonNull(methodInfo, "method info " + spanInfo.getContainingMethod() + " must exist in DocumentInfo for " + documentInfo.getFileUri());
@@ -423,7 +443,7 @@ private void withSpanAnnotationSpanDiscovery(@NotNull PsiFile psiFile, @NotNull
423443
List<SpanInfo> spanInfos = JavaSpanDiscoveryUtils.getSpanInfoFromWithSpanAnnotatedMethod(psiMethod);
424444
if (spanInfos != null) {
425445
spanInfos.forEach(spanInfo -> {
426-
Log.log(LOGGER::debug, "Found span info {} for method {}",spanInfo.getId(),spanInfo.getContainingMethod());
446+
Log.log(LOGGER::debug, "Found span info {} for method {}", spanInfo.getId(), spanInfo.getContainingMethod());
427447
MethodInfo methodInfo = documentInfo.getMethods().get(spanInfo.getContainingMethod());
428448
//this method must exist in the document info
429449
Objects.requireNonNull(methodInfo, "method info " + spanInfo.getContainingMethod() + " must exist in DocumentInfo for " + documentInfo.getFileUri());

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

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

33

44
import com.intellij.openapi.project.Project;
5-
import com.intellij.psi.*;
5+
import com.intellij.psi.PsiAssignmentExpression;
6+
import com.intellij.psi.PsiClass;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.PsiExpression;
9+
import com.intellij.psi.PsiFile;
10+
import com.intellij.psi.PsiJavaFile;
11+
import com.intellij.psi.PsiMethod;
12+
import com.intellij.psi.PsiMethodCallExpression;
13+
import com.intellij.psi.PsiReference;
14+
import com.intellij.psi.PsiReferenceExpression;
15+
import com.intellij.psi.PsiVariable;
616
import com.intellij.psi.search.GlobalSearchScope;
717
import com.intellij.psi.search.searches.ReferencesSearch;
818
import com.intellij.psi.util.PsiTreeUtil;
@@ -16,8 +26,21 @@
1626
import java.util.List;
1727
import java.util.Objects;
1828

19-
import static org.digma.intellij.plugin.idea.psi.java.Constants.*;
20-
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.*;
29+
import static org.digma.intellij.plugin.idea.psi.java.Constants.OPENTELEMETY_FQN;
30+
import static org.digma.intellij.plugin.idea.psi.java.Constants.SPAN_BUILDER_FQN;
31+
import static org.digma.intellij.plugin.idea.psi.java.Constants.TRACER_BUILDER_FQN;
32+
import static org.digma.intellij.plugin.idea.psi.java.Constants.TRACER_FQN;
33+
import static org.digma.intellij.plugin.idea.psi.java.Constants.WITH_SPAN_INST_LIBRARY_1;
34+
import static org.digma.intellij.plugin.idea.psi.java.Constants.WITH_SPAN_INST_LIBRARY_2;
35+
import static org.digma.intellij.plugin.idea.psi.java.Constants.WITH_SPAN_INST_LIBRARY_3;
36+
import static org.digma.intellij.plugin.idea.psi.java.Constants.WITH_SPAN_INST_LIBRARY_4;
37+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.createJavaMethodCodeObjectId;
38+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.createSpanIdForWithSpanAnnotation;
39+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.createSpanIdFromInstLibraryAndSpanName;
40+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.createSpanNameForWithSpanAnnotation;
41+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.getValueFromFirstArgument;
42+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.isMethodWithFirstArgumentString;
43+
import static org.digma.intellij.plugin.idea.psi.java.JavaLanguageUtils.isMethodWithNoArguments;
2144

2245
/**
2346
* Utility methods for span discovery.
@@ -63,6 +86,7 @@ public static List<SpanInfo> getSpanInfoFromWithSpanAnnotatedMethod(@NotNull Psi
6386
spanInfos.add(new SpanInfo(createSpanIdForWithSpanAnnotation(psiMethod, withSpanAnnotation, containingClass,WITH_SPAN_INST_LIBRARY_1), spanName, methodId, containingFileUri));
6487
spanInfos.add(new SpanInfo(createSpanIdForWithSpanAnnotation(psiMethod, withSpanAnnotation, containingClass,WITH_SPAN_INST_LIBRARY_2), spanName, methodId, containingFileUri));
6588
spanInfos.add(new SpanInfo(createSpanIdForWithSpanAnnotation(psiMethod, withSpanAnnotation, containingClass,WITH_SPAN_INST_LIBRARY_3), spanName, methodId, containingFileUri));
89+
spanInfos.add(new SpanInfo(createSpanIdForWithSpanAnnotation(psiMethod, withSpanAnnotation, containingClass,WITH_SPAN_INST_LIBRARY_4), spanName, methodId, containingFileUri));
6690
return spanInfos;
6791
}
6892

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package org.digma.intellij.plugin.idea.psi.java;
2+
3+
import com.intellij.lang.jvm.annotation.JvmAnnotationAttribute;
4+
import com.intellij.openapi.diagnostic.Logger;
5+
import com.intellij.openapi.project.Project;
6+
import com.intellij.psi.JavaPsiFacade;
7+
import com.intellij.psi.PsiAnnotation;
8+
import com.intellij.psi.PsiClass;
9+
import com.intellij.psi.PsiFile;
10+
import com.intellij.psi.PsiMethod;
11+
import com.intellij.psi.search.GlobalSearchScope;
12+
import com.intellij.psi.search.searches.AnnotatedElementsSearch;
13+
import com.intellij.util.Query;
14+
import org.digma.intellij.plugin.log.Log;
15+
import org.digma.intellij.plugin.model.discovery.DocumentInfo;
16+
import org.digma.intellij.plugin.model.discovery.EndpointInfo;
17+
import org.digma.intellij.plugin.model.discovery.MethodInfo;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
import java.util.List;
22+
import java.util.Objects;
23+
import java.util.stream.Collectors;
24+
25+
public class MicronautFramework {
26+
27+
private static final Logger LOGGER = Logger.getInstance(MicronautFramework.class);
28+
private static final String CONTROLLER_ANNOTATION_STR = "io.micronaut.http.annotation.Controller";
29+
private static final String HTTP_DELETE_ANNOTATION_STR = "io.micronaut.http.annotation.Delete";
30+
private static final String HTTP_GET_ANNOTATION_STR = "io.micronaut.http.annotation.Get";
31+
private static final String HTTP_HEAD_ANNOTATION_STR = "io.micronaut.http.annotation.Head";
32+
private static final String HTTP_OPTIONS_ANNOTATION_STR = "io.micronaut.http.annotation.Options";
33+
private static final String HTTP_PATCH_ANNOTATION_STR = "io.micronaut.http.annotation.Patch";
34+
private static final String HTTP_POST_ANNOTATION_STR = "io.micronaut.http.annotation.Post";
35+
private static final String HTTP_PUT_ANNOTATION_STR = "io.micronaut.http.annotation.Put";
36+
private static final String HTTP_TRACE_ANNOTATION_STR = "io.micronaut.http.annotation.Trace";
37+
private static final List<String> HTTP_METHODS_ANNOTATION_STR_LIST = List.of(
38+
HTTP_DELETE_ANNOTATION_STR, HTTP_GET_ANNOTATION_STR, HTTP_HEAD_ANNOTATION_STR, HTTP_OPTIONS_ANNOTATION_STR,
39+
HTTP_PATCH_ANNOTATION_STR, HTTP_POST_ANNOTATION_STR, HTTP_PUT_ANNOTATION_STR, HTTP_TRACE_ANNOTATION_STR);
40+
41+
private static final List<String> VALUE_OR_URI = List.of("value", "uri");
42+
43+
private final Project project;
44+
45+
// late init
46+
private boolean lateInitAlready = false;
47+
private PsiClass controllerAnnotationClass;
48+
private List<JavaAnnotation> httpMethodsAnnotations;
49+
50+
public MicronautFramework(Project project) {
51+
this.project = project;
52+
}
53+
54+
private void lateInit() {
55+
if (lateInitAlready) return;
56+
57+
JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
58+
controllerAnnotationClass = psiFacade.findClass(CONTROLLER_ANNOTATION_STR, GlobalSearchScope.allScope(project));
59+
initHttpMethodAnnotations(psiFacade);
60+
61+
lateInitAlready = true;
62+
}
63+
64+
private void initHttpMethodAnnotations(JavaPsiFacade psiFacade) {
65+
httpMethodsAnnotations = HTTP_METHODS_ANNOTATION_STR_LIST.stream()
66+
.map(currFqn -> {
67+
PsiClass psiClass = psiFacade.findClass(currFqn, GlobalSearchScope.allScope(project));
68+
if (psiClass == null) return null;
69+
return new JavaAnnotation(currFqn, psiClass);
70+
})
71+
.filter(Objects::nonNull)
72+
.collect(Collectors.toUnmodifiableList());
73+
}
74+
75+
private boolean isMicronautHttpRelevant() {
76+
return controllerAnnotationClass != null;
77+
}
78+
79+
public void endpointDiscovery(@NotNull PsiFile psiFile, @NotNull DocumentInfo documentInfo) {
80+
lateInit();
81+
if (!isMicronautHttpRelevant()) {
82+
return;
83+
}
84+
85+
httpMethodsAnnotations.forEach(currAnnotation -> {
86+
Query<PsiMethod> psiMethodsInFile = AnnotatedElementsSearch.searchPsiMethods(currAnnotation.getPsiClass(), GlobalSearchScope.fileScope(psiFile));
87+
88+
for (PsiMethod currPsiMethod : psiMethodsInFile) {
89+
PsiClass controllerClass = currPsiMethod.getContainingClass();
90+
if (controllerClass == null) {
91+
continue; // very unlikely
92+
}
93+
PsiAnnotation controllerAnnotation = controllerClass.getAnnotation(CONTROLLER_ANNOTATION_STR);
94+
if (controllerAnnotation == null) {
95+
continue; // skip this method, since its class is not a controller
96+
}
97+
String endpointUriPrefix = JavaLanguageUtils.getPsiAnnotationAttributeValue(controllerAnnotation, "value");
98+
99+
String methodCodeObjectId = JavaLanguageUtils.createJavaMethodCodeObjectId(currPsiMethod);
100+
String httpEndpointCodeObjectId = createHttpEndpointCodeObjectId(currPsiMethod, currAnnotation, endpointUriPrefix);
101+
if (httpEndpointCodeObjectId == null) {
102+
continue; // skip this method, since endpoint value could not be determined
103+
}
104+
105+
EndpointInfo endpointInfo = new EndpointInfo(httpEndpointCodeObjectId, methodCodeObjectId, documentInfo.getFileUri());
106+
Log.log(LOGGER::debug, "Found endpoint info '{}' for method '{}'", endpointInfo.getId(), endpointInfo.getContainingMethodId());
107+
108+
MethodInfo methodInfo = documentInfo.getMethods().get(endpointInfo.getContainingMethodId());
109+
//this method must exist in the document info
110+
Objects.requireNonNull(methodInfo, "method info " + endpointInfo.getContainingMethodId() + " must exist in DocumentInfo for " + documentInfo.getFileUri());
111+
methodInfo.addEndpoint(endpointInfo);
112+
}
113+
});
114+
}
115+
116+
@Nullable
117+
protected static String createHttpEndpointCodeObjectId(PsiMethod psiMethod, JavaAnnotation httpMethodAnnotation, String endpointUriPrefix) {
118+
PsiAnnotation httpPsiAnnotation = psiMethod.getAnnotation(httpMethodAnnotation.getClassNameFqn());
119+
List<JvmAnnotationAttribute> annotationAttributes = httpPsiAnnotation.getAttributes();
120+
121+
String endpointUriSuffix = "/";
122+
for (JvmAnnotationAttribute curr : annotationAttributes) {
123+
// taking the first attribute, either "value" or "uri" - that's how micronaut behave if both exists
124+
if (VALUE_OR_URI.contains(curr.getAttributeName())) {
125+
endpointUriSuffix = JavaLanguageUtils.getPsiAnnotationAttributeValue(httpPsiAnnotation, curr.getAttributeName());
126+
if (endpointUriSuffix == null) {
127+
Log.log(LOGGER::debug, "cannot create http endpoint for method '{}' since could not extract attribute value for name '{}' from annotation '{}'",
128+
psiMethod.getName(), curr.getAttributeName(), httpMethodAnnotation.getClassNameFqn());
129+
return null; // unlikely
130+
}
131+
break; // found the first occurrence, and out
132+
}
133+
// note: attribute of "uris" is irrelevant
134+
}
135+
136+
String httpMethodUcase = getHttpMethod(httpMethodAnnotation).toUpperCase();
137+
138+
// value for example : 'epHTTP:HTTP GET GET - /books/get'
139+
return "" +
140+
// digma part
141+
"epHTTP:" + "HTTP " + httpMethodUcase + " " +
142+
// Micronaut part
143+
httpMethodUcase + " - " + JavaUtils.combineUri(endpointUriPrefix, endpointUriSuffix);
144+
}
145+
146+
@NotNull
147+
private static String getHttpMethod(JavaAnnotation javaAnnotation) {
148+
String fqn = javaAnnotation.getClassNameFqn();
149+
int lastIndexOfDot = fqn.lastIndexOf('.');
150+
if (lastIndexOfDot >= 0) {
151+
return fqn.substring(lastIndexOfDot + 1);
152+
} else {
153+
return fqn;
154+
}
155+
}
156+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.digma.intellij.plugin.idea.psi.java
2+
3+
import com.intellij.psi.PsiClass
4+
5+
data class JavaAnnotation(
6+
val classNameFqn: String,
7+
val psiClass: PsiClass,
8+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.digma.intellij.plugin.idea.psi.java
2+
3+
class JavaUtils {
4+
5+
companion object {
6+
7+
/**
8+
* combineUri. used for combining controller defined route and specific endpoint
9+
* the output of it will always start with / .
10+
*/
11+
@JvmStatic
12+
fun combineUri(prefix: String?, suffix: String?): String {
13+
if (suffix.isNullOrBlank()) {
14+
return adjustUri(prefix)
15+
}
16+
if (prefix.isNullOrBlank()) {
17+
return adjustUri(suffix)
18+
}
19+
return adjustUri(adjustUri(prefix) + adjustUri(suffix))
20+
}
21+
22+
/**
23+
* adjustUri.
24+
* 1. trim leading and trailing spaces
25+
* 2. make sure it starts with /
26+
* 3. make sure it does not end with / (unless its only /)
27+
* 4. replace all // to single /
28+
*/
29+
@JvmStatic
30+
fun adjustUri(value: String?): String {
31+
if (value.isNullOrBlank()) return "/"
32+
var adjusted = value.trim().trimEnd('/').trimStart('/').trim()
33+
adjusted = adjusted.replace("/////", "/")
34+
adjusted = adjusted.replace("////", "/")
35+
adjusted = adjusted.replace("///", "/")
36+
adjusted = adjusted.replace("//", "/")
37+
if (adjusted.startsWith("/")) {
38+
return adjusted
39+
} else {
40+
return "/$adjusted"
41+
}
42+
}
43+
}
44+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.digma.intellij.plugin.idea.psi.java
2+
3+
import org.digma.intellij.plugin.idea.psi.java.JavaUtils.Companion.adjustUri
4+
import org.digma.intellij.plugin.idea.psi.java.JavaUtils.Companion.combineUri
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
8+
internal class JavaUtilsTest {
9+
10+
@Test
11+
fun combineUri_given_empty_strings() {
12+
assertEquals("/", combineUri("", ""))
13+
assertEquals("/abc", combineUri("abc", ""))
14+
assertEquals("/qrs", combineUri("", "qrs"))
15+
}
16+
17+
@Test
18+
fun combineUri_various_combinations() {
19+
assertEquals("/abc/qrs", combineUri("abc", "qrs"))
20+
assertEquals("/abc/qrs", combineUri("/abc", "/qrs"))
21+
assertEquals("/abc/qrs", combineUri("abc/", "qrs/"))
22+
assertEquals("/abc/qrs", combineUri("/abc/", "/qrs/"))
23+
}
24+
25+
@Test
26+
fun adjustUri_various_options() {
27+
assertEquals("/abc/qrs", adjustUri("abc///qrs////"))
28+
assertEquals("/abc/qrs", adjustUri(" abc/qrs "))
29+
}
30+
}

0 commit comments

Comments
 (0)