Skip to content

Commit 72a4865

Browse files
committed
GH-1649: added validation for version attribute with regards to versioning config exists or not
1 parent a51d4bd commit 72a4865

File tree

9 files changed

+554
-15
lines changed

9 files changed

+554
-15
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
@@ -53,6 +53,7 @@
5353
import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler;
5454
import org.springframework.ide.vscode.boot.java.reconcilers.ServerHttpSecurityLambdaDslReconciler;
5555
import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler;
56+
import org.springframework.ide.vscode.boot.java.reconcilers.WebApiVersioningReconciler;
5657
import org.springframework.ide.vscode.boot.java.reconcilers.WebSecurityConfigurerAdapterReconciler;
5758
import org.springframework.ide.vscode.boot.java.semantictokens.EmbeddedLanguagesSemanticTokensSupport;
5859
import org.springframework.ide.vscode.boot.java.semantictokens.JavaSemanticTokensProvider;
@@ -100,6 +101,10 @@ public class JdtConfig {
100101
return new PathInControllerAnnotationReconciler();
101102
}
102103

104+
@Bean WebApiVersioningReconciler webApiVersioningReconciler(SpringMetamodelIndex springIndex) {
105+
return new WebApiVersioningReconciler(springIndex);
106+
}
107+
103108
@Bean WebSecurityConfigurerAdapterReconciler webSecurityConfigurerAdapterReconciler(SimpleLanguageServer server) {
104109
return new WebSecurityConfigurerAdapterReconciler(server.getQuickfixRegistry());
105110
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
public enum Boot4JavaProblemType implements ProblemType {
2424

2525
REGISTRAR_BEAN_INVALID_ANNOTATION(WARNING, "Bean Registrar cannot be registered as a bean via `@Component` annotations", "Invalid annotation over bean registrar"),
26-
REGISTRAR_BEAN_DECLARATION(WARNING, "Bean Registrar should be added to a configurarion bean via `@Import`", "Not added to configurartion via `@Import`");
26+
REGISTRAR_BEAN_DECLARATION(WARNING, "Bean Registrar should be added to a configurarion bean via `@Import`", "Not added to configurartion via `@Import`"),
27+
API_VERSIONING_NOT_CONFIGURED(WARNING, "API Versioning used but not configured anywhere", "API Versioning not configured anywhere");
2728

2829
private final ProblemSeverity defaultSeverity;
2930
private final String description;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.reconcilers;
12+
13+
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
14+
15+
import java.net.URI;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
19+
import org.eclipse.jdt.core.dom.ASTVisitor;
20+
import org.eclipse.jdt.core.dom.CompilationUnit;
21+
import org.eclipse.jdt.core.dom.IAnnotationBinding;
22+
import org.eclipse.jdt.core.dom.ITypeBinding;
23+
import org.eclipse.jdt.core.dom.MemberValuePair;
24+
import org.eclipse.jdt.core.dom.NormalAnnotation;
25+
import org.eclipse.jdt.core.dom.TypeDeclaration;
26+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
27+
import org.springframework.ide.vscode.boot.java.Annotations;
28+
import org.springframework.ide.vscode.boot.java.Boot4JavaProblemType;
29+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
30+
import org.springframework.ide.vscode.boot.java.requestmapping.WebConfigIndexElement;
31+
import org.springframework.ide.vscode.boot.java.requestmapping.WebConfigIndexer;
32+
import org.springframework.ide.vscode.commons.java.IJavaProject;
33+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
34+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
35+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
36+
import org.springframework.ide.vscode.commons.util.UriUtil;
37+
38+
public class WebApiVersioningReconciler implements JdtAstReconciler {
39+
40+
private static final String PROBLEM_LABEL = "API versioning not configured anywhere";
41+
42+
private final SpringMetamodelIndex springIndex;
43+
44+
public WebApiVersioningReconciler(SpringMetamodelIndex springIndex) {
45+
this.springIndex = springIndex;
46+
}
47+
48+
@Override
49+
public boolean isApplicable(IJavaProject project) {
50+
return springBootVersionGreaterOrEqual(4, 0, 0).test(project);
51+
}
52+
53+
@Override
54+
public ProblemType getProblemType() {
55+
return Boot4JavaProblemType.API_VERSIONING_NOT_CONFIGURED;
56+
}
57+
58+
@Override
59+
public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, ReconcilingContext context) {
60+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu);
61+
62+
return new ASTVisitor() {
63+
64+
/**
65+
* this in the piece of the reconciler that validates the version attribute of annotations on controllers
66+
*/
67+
@Override
68+
public boolean visit(NormalAnnotation annotation) {
69+
IAnnotationBinding annotationBinding = annotation.resolveAnnotationBinding();
70+
if (annotationBinding == null) return super.visit(annotation);
71+
72+
ITypeBinding annotationType = annotationBinding.getAnnotationType();
73+
if (annotationType == null) return super.visit(annotation);
74+
75+
boolean isWebController = annotationHierarchies.isAnnotatedWith(annotationType, Annotations.SPRING_REQUEST_MAPPING);
76+
if (!isWebController) return super.visit(annotation);
77+
78+
@SuppressWarnings("unchecked")
79+
List<MemberValuePair> attributes = annotation.values();
80+
attributes.stream()
81+
.filter(pair -> "version".equals(pair.getName().toString()))
82+
.forEach(pair -> {
83+
if (!context.isIndexComplete()) {
84+
throw new RequiredCompleteIndexException();
85+
}
86+
87+
if (!isApiVersioningConfigured(project)) {
88+
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL,
89+
pair.getStartPosition(), pair.getLength());
90+
91+
context.getProblemCollector().accept(problem);
92+
}
93+
}
94+
);
95+
96+
return super.visit(annotation);
97+
}
98+
99+
/**
100+
* this is the piece of the reconciler that looks for changes to web configs and then marks all the potentially affected
101+
* controller classes for re-indexing
102+
*/
103+
@Override
104+
public boolean visit(TypeDeclaration type) {
105+
if (WebConfigIndexer.getWebConfig(type) == null) {
106+
return super.visit(type);
107+
}
108+
109+
Arrays.stream(springIndex.getBeansOfProject(project.getElementName()))
110+
.filter(bean -> isAnnotatedWith(bean, Annotations.CONTROLLER))
111+
.map(bean -> UriUtil.toFileString(bean.getLocation().getUri()))
112+
.forEach(file -> context.markForAffetcedFilesIndexing(file));
113+
114+
return super.visit(type);
115+
}
116+
};
117+
}
118+
119+
private boolean isApiVersioningConfigured(IJavaProject project) {
120+
List<WebConfigIndexElement> webConfigs = springIndex.getNodesOfType(project.getElementName(), WebConfigIndexElement.class);
121+
for (WebConfigIndexElement webConfig : webConfigs) {
122+
if (webConfig.getVersionSupportStrategies() != null && webConfig.getVersionSupportStrategies().size() > 0) {
123+
return true;
124+
}
125+
}
126+
127+
return false;
128+
}
129+
130+
private boolean isAnnotatedWith(Bean bean, String annotationType) {
131+
return Arrays.stream(bean.getAnnotations())
132+
.filter(annotation -> annotation.getAnnotationType().equals(annotationType))
133+
.findAny()
134+
.isPresent();
135+
}
136+
137+
}

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,17 @@ public class WebConfigIndexer {
4242
private static Map<String, MethodInvocationExtractor> methodExtractors = initializeMethodExtractors();
4343

4444
public static void indexWebConfig(Bean beanDefinition, TypeDeclaration type, SpringIndexerJavaContext context, TextDocument doc) {
45-
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
46-
47-
ITypeBinding binding = type.resolveBinding();
48-
if (binding == null) return;
49-
50-
if (!annotationHierarchies.isAnnotatedWith(binding, Annotations.CONFIGURATION)) {
51-
return;
52-
}
53-
54-
ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(binding, Set.of(Annotations.WEB_MVC_CONFIGURER_INTERFACE));
55-
if (inTypeHierarchy == null) {
45+
ITypeBinding webmvcConfigType = getWebConfig(type);
46+
if (webmvcConfigType == null) {
5647
return;
5748
}
5849

5950
if (!context.isFullAst()) { // needs full method bodies to continue
6051
throw new RequiredCompleteAstException();
6152
}
6253

63-
MethodDeclaration configureVersioningMethod = findMethod(type, inTypeHierarchy, CONFIGURE_API_VERSIONING_METHOD);
64-
MethodDeclaration configurePathMethod = findMethod(type, inTypeHierarchy, CONFIGURE_PATH_MATCHING_METHOD);
65-
54+
MethodDeclaration configureVersioningMethod = findMethod(type, webmvcConfigType, CONFIGURE_API_VERSIONING_METHOD);
55+
MethodDeclaration configurePathMethod = findMethod(type, webmvcConfigType, CONFIGURE_PATH_MATCHING_METHOD);
6656

6757
if (configureVersioningMethod != null || configurePathMethod != null) {
6858
Builder builder = new WebConfigIndexElement.Builder(ConfigType.WEB_CONFIG);
@@ -77,6 +67,19 @@ public static void indexWebConfig(Bean beanDefinition, TypeDeclaration type, Spr
7767
}
7868

7969
}
70+
71+
public static ITypeBinding getWebConfig(TypeDeclaration type) {
72+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(type);
73+
74+
ITypeBinding binding = type.resolveBinding();
75+
if (binding == null) return null;
76+
77+
if (!annotationHierarchies.isAnnotatedWith(binding, Annotations.CONFIGURATION)) {
78+
return null;
79+
}
80+
81+
return ASTUtils.findInTypeHierarchy(binding, Set.of(Annotations.WEB_MVC_CONFIGURER_INTERFACE));
82+
}
8083

8184
private static void scanMethodBody(Builder builder, Block body, SpringIndexerJavaContext context, TextDocument doc) {
8285
if (body == null) {

0 commit comments

Comments
 (0)