Skip to content

Commit ca4fc3c

Browse files
committed
enable feign client and feign config indexing and reconciling to keep track of dependencies and trigger re-reconciling
1 parent f08bfb3 commit ca4fc3c

File tree

10 files changed

+310
-17
lines changed

10 files changed

+310
-17
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.ide.vscode.boot.java.reconcilers.BeanRegistrarDeclarationReconciler;
3939
import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler;
4040
import org.springframework.ide.vscode.boot.java.reconcilers.EntityIdForRepoReconciler;
41+
import org.springframework.ide.vscode.boot.java.reconcilers.FeignClientReconciler;
4142
import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler;
4243
import org.springframework.ide.vscode.boot.java.reconcilers.ImplicitWebAnnotationNamesReconciler;
4344
import org.springframework.ide.vscode.boot.java.reconcilers.ModulithTypeReferenceViolationReconciler;
@@ -74,6 +75,10 @@ public class JdtConfig {
7475
return new BeanRegistrarDeclarationReconciler(server.getQuickfixRegistry(), springIndex);
7576
}
7677

78+
@Bean FeignClientReconciler feignClientReconciler(SpringMetamodelIndex springIndex) {
79+
return new FeignClientReconciler(springIndex);
80+
}
81+
7782
@Bean AutowiredFieldIntoConstructorParameterReconciler autowiredFieldIntoConstructorParameterReconciler(SimpleLanguageServer server) {
7883
return new AutowiredFieldIntoConstructorParameterReconciler(server.getQuickfixRegistry());
7984
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.Arrays;
1414
import java.util.Collection;
1515
import java.util.HashSet;
16+
import java.util.List;
1617
import java.util.Set;
1718
import java.util.stream.Collectors;
1819
import java.util.stream.Stream;
@@ -31,10 +32,13 @@
3132
import org.eclipse.lsp4j.jsonrpc.messages.Tuple.Two;
3233
import org.slf4j.Logger;
3334
import org.slf4j.LoggerFactory;
35+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
36+
import org.springframework.ide.vscode.boot.java.Annotations;
3437
import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider;
3538
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
3639
import org.springframework.ide.vscode.boot.java.utils.CachedSymbol;
3740
import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext;
41+
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationAttributeValue;
3842
import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata;
3943
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
4044
import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint;
@@ -53,15 +57,31 @@ public void addSymbols(Annotation node, ITypeBinding annotationType, Collection<
5357

5458
WorkspaceSymbol symbol = result.getFirst();
5559
Bean beanDefinition = result.getSecond();
60+
5661
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol));
5762
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
63+
64+
markNewFeignConfigTypeForReconciling(beanDefinition, context);
5865
}
5966
}
6067
catch (BadLocationException e) {
6168
log.error("", e);
6269
}
6370
}
6471

72+
private void markNewFeignConfigTypeForReconciling(Bean beanDefinition, SpringIndexerJavaContext context) {
73+
List<String> configurationTypes = Arrays.stream(beanDefinition.getAnnotations())
74+
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
75+
.map(annotation -> annotation.getAttributes())
76+
.filter(attributes -> attributes.containsKey("configuration"))
77+
.map(attributes -> attributes.get("configuration"))
78+
.flatMap(attributeValues -> Arrays.stream(attributeValues))
79+
.map(attributeValue -> attributeValue.getName())
80+
.toList();
81+
82+
83+
}
84+
6585
private Two<WorkspaceSymbol, Bean> createSymbol(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, TextDocument doc) throws BadLocationException {
6686
String annotationTypeName = annotationType.getName();
6787
Collection<String> metaAnnotationNames = metaAnnotations.stream()
@@ -94,7 +114,7 @@ private Two<WorkspaceSymbol, Bean> createSymbol(Annotation node, ITypeBinding an
94114
.toArray(AnnotationMetadata[]::new);
95115

96116
Bean beanDefinition = new Bean(beanName, beanType == null ? "" : beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, false, symbol.getName());
97-
117+
98118
return Tuple.two(symbol, beanDefinition);
99119
}
100120

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUni
6969

7070
@Override
7171
public boolean visit(TypeDeclaration classDecl) {
72-
if (isApplicableClass(project, cu, classDecl)) {
72+
if (isApplicableClass(project, cu, classDecl, context)) {
7373
SimpleName nameAst = classDecl.getName();
7474
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL,
7575
nameAst.getStartPosition(), nameAst.getLength());
@@ -92,7 +92,7 @@ public boolean visit(TypeDeclaration classDecl) {
9292
};
9393
}
9494

95-
private boolean isApplicableClass(IJavaProject project, CompilationUnit cu, TypeDeclaration classDecl) {
95+
private boolean isApplicableClass(IJavaProject project, CompilationUnit cu, TypeDeclaration classDecl, ReconcilingContext context) {
9696
if (classDecl.isInterface()) {
9797
return false;
9898
}
@@ -128,15 +128,22 @@ private boolean isApplicableClass(IJavaProject project, CompilationUnit cu, Type
128128

129129
// No '@Configuration' present. Check if any methods have '@Bean' annotation
130130
for (MethodDeclaration m : classDecl.getMethods()) {
131-
if (isBeanMethod(m) && !isException(project, classDecl)) {
132-
return true;
131+
if (isBeanMethod(m)) {
132+
if (context.isIndexComplete()) {
133+
if (!isException(project, classDecl, context)) {
134+
return true;
135+
}
136+
}
137+
else {
138+
throw new RequiredCompleteIndexException();
139+
}
133140
}
134141
}
135142

136143
return false;
137144
}
138145

139-
private boolean isException(IJavaProject project, TypeDeclaration classDecl) {
146+
private boolean isException(IJavaProject project, TypeDeclaration classDecl, ReconcilingContext context) {
140147
if (springIndex == null) {
141148
return false;
142149
}
@@ -154,19 +161,30 @@ private boolean isException(IJavaProject project, TypeDeclaration classDecl) {
154161
return true;
155162
}
156163

157-
boolean isConfiguredAsfeignConfigClass = Arrays.stream(beans)
158-
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
159-
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
160-
.map(annotation -> annotation.getAttributes().get("configuration"))
161-
.flatMap(attributeValues -> attributeValues != null ? Arrays.stream(attributeValues) : Stream.empty())
162-
.anyMatch(attributeValue -> attributeValue.getName().equals(beanClassName));
164+
List<Bean> feignClients = Arrays.stream(beans)
165+
.filter(bean -> isConfiguredAsFeignConfigClass(bean, beanClassName))
166+
.toList();
167+
168+
if (!feignClients.isEmpty()) {
163169

164-
if (isConfiguredAsfeignConfigClass) {
170+
// record dependency for feign clients that have this type configured as feign configuration
171+
for (Bean feignClient : feignClients) {
172+
context.addDependency(feignClient.getType());
173+
}
174+
165175
return true;
166176
}
167177

168178
return false;
169179
}
180+
181+
private boolean isConfiguredAsFeignConfigClass(Bean bean, String beanClassName) {
182+
return Arrays.stream(bean.getAnnotations())
183+
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
184+
.map(annotation -> annotation.getAttributes().get("configuration"))
185+
.flatMap(attributeValues -> attributeValues != null ? Arrays.stream(attributeValues) : Stream.empty())
186+
.anyMatch(attributeValue -> attributeValue.getName().equals(beanClassName));
187+
}
170188

171189
private static boolean isBeanMethod(MethodDeclaration m) {
172190
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(m);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
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+
* VMware, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.reconcilers;
12+
13+
import java.net.URI;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
17+
import org.eclipse.jdt.core.dom.ASTVisitor;
18+
import org.eclipse.jdt.core.dom.CompilationUnit;
19+
import org.eclipse.jdt.core.dom.TypeDeclaration;
20+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
21+
import org.springframework.ide.vscode.boot.java.Annotations;
22+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
23+
import org.springframework.ide.vscode.commons.java.IJavaProject;
24+
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
25+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
26+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
27+
import org.springframework.ide.vscode.commons.protocol.spring.BeanMethodContainerElement;
28+
import org.springframework.ide.vscode.commons.protocol.spring.SpringIndexElement;
29+
import org.springframework.ide.vscode.commons.util.UriUtil;
30+
31+
public class FeignClientReconciler implements JdtAstReconciler {
32+
33+
private final SpringMetamodelIndex springIndex;
34+
35+
public FeignClientReconciler(SpringMetamodelIndex springIndex) {
36+
this.springIndex = springIndex;
37+
}
38+
39+
@Override
40+
public boolean isApplicable(IJavaProject project) {
41+
return SpringProjectUtil.hasDependencyStartingWith(project, "spring-cloud-openfeign-core", null);
42+
}
43+
44+
@Override
45+
public ProblemType getProblemType() {
46+
return null;
47+
}
48+
49+
@Override
50+
public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUnit cu, ReconcilingContext context) {
51+
return new ASTVisitor() {
52+
53+
@Override
54+
public boolean visit(TypeDeclaration node) {
55+
AnnotationHierarchies annotationHierarchy = AnnotationHierarchies.get(node);
56+
57+
if (!annotationHierarchy.isAnnotatedWith(node.resolveBinding(), Annotations.FEIGN_CLIENT)) {
58+
return true;
59+
}
60+
61+
List<SpringIndexElement> elements = context.getCreatedIndexElements();
62+
63+
List<String> configurationTypes = elements.stream()
64+
.filter(element -> element instanceof Bean)
65+
.map(element -> (Bean) element)
66+
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
67+
.filter(annotation -> annotation.getAnnotationType().equals(Annotations.FEIGN_CLIENT))
68+
.map(annotation -> annotation.getAttributes())
69+
.filter(attributes -> attributes.containsKey("configuration"))
70+
.flatMap(attributes -> Arrays.stream(attributes.get("configuration")))
71+
.map(attribute -> attribute.getName())
72+
.toList();
73+
74+
List<BeanMethodContainerElement> beanMethodContainers = springIndex.getNodesOfType(BeanMethodContainerElement.class);
75+
76+
beanMethodContainers.stream().filter(beanContainer -> configurationTypes.contains(beanContainer.getType()))
77+
.map(beanContainer -> beanContainer.getLocation().getUri()).map(docURI -> UriUtil.toFileString(docURI))
78+
.forEach(file -> context.markForAffetcedFilesIndexing(file));
79+
80+
return true;
81+
}
82+
83+
};
84+
}
85+
86+
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcilingContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ public boolean isIndexComplete() {
6161
return isIndexComplete;
6262
}
6363

64-
public void addDependency(String typeOfConfigClassWithImport) {
65-
this.dependencies.add(typeOfConfigClassWithImport);
64+
public void addDependency(String type) {
65+
this.dependencies.add(type);
6666
}
6767

6868
public Set<String> getDependencies() {

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaDependencyTracker.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ public void dump() {
4141
public Multimap<String, String> getAllDependencies() {
4242
return dependencies;
4343
}
44+
45+
public Collection<String> get(String file) {
46+
return dependencies.get(file);
47+
}
4448

4549
public void update(String file, Set<String> dependenciesForFile) {
4650
dependencies.replaceValues(file, dependenciesForFile);

0 commit comments

Comments
 (0)