Skip to content

Commit 4e4e31c

Browse files
committed
GH-1880: take custom API versioning mechanism configured via useVersionResolver into account
1 parent 38e6679 commit 4e4e31c

3 files changed

Lines changed: 159 additions & 2 deletions

File tree

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@
1919
import org.eclipse.jdt.core.dom.ASTNode;
2020
import org.eclipse.jdt.core.dom.ASTVisitor;
2121
import org.eclipse.jdt.core.dom.Block;
22+
import org.eclipse.jdt.core.dom.CastExpression;
23+
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
2224
import org.eclipse.jdt.core.dom.Expression;
25+
import org.eclipse.jdt.core.dom.IBinding;
2326
import org.eclipse.jdt.core.dom.IMethodBinding;
2427
import org.eclipse.jdt.core.dom.ITypeBinding;
28+
import org.eclipse.jdt.core.dom.IVariableBinding;
29+
import org.eclipse.jdt.core.dom.LambdaExpression;
2530
import org.eclipse.jdt.core.dom.MethodDeclaration;
2631
import org.eclipse.jdt.core.dom.MethodInvocation;
32+
import org.eclipse.jdt.core.dom.Name;
2733
import org.eclipse.jdt.core.dom.TypeDeclaration;
2834
import org.eclipse.lsp4j.Range;
2935
import org.springframework.ide.vscode.boot.java.Annotations;
@@ -50,13 +56,15 @@ public class WebConfigJavaIndexer {
5056
public static final String USE_PATH_SEGMENT = "usePathSegment";
5157
public static final String USE_REQUEST_HEADER = "useRequestHeader";
5258
public static final String USE_MEDIA_TYPE_PARAMETER = "useMediaTypeParameter";
59+
public static final String USE_VERSION_RESOLVER = "useVersionResolver";
5360
public static final String ADD_SUPPORTED_VERSIONS = "addSupportedVersions";
5461

5562
public static final Set<String> VERSIONING_CONFIG_METHODS = Set.of(
5663
USE_PATH_SEGMENT,
5764
USE_QUERY_PARAM,
5865
USE_REQUEST_HEADER,
59-
USE_MEDIA_TYPE_PARAMETER
66+
USE_MEDIA_TYPE_PARAMETER,
67+
USE_VERSION_RESOLVER
6068
);
6169

6270

@@ -241,6 +249,37 @@ private static Map<String, MethodInvocationExtractor> initializeMethodExtractors
241249
}
242250
}));
243251

252+
result.put(USE_VERSION_RESOLVER, new MethodInvocationExtractor() {
253+
254+
@Override
255+
public Set<String> getTargetInvocationType() {
256+
return apiConfigurerInterfaces;
257+
}
258+
259+
@Override
260+
public void extractParameters(TextDocument doc, MethodInvocation methodInvocation, Builder webconfigBuilder) {
261+
@SuppressWarnings("unchecked")
262+
List<Expression> arguments = methodInvocation.arguments();
263+
if (arguments.isEmpty()) {
264+
recordVersionResolver(doc, methodInvocation, webconfigBuilder, "unspecified");
265+
return;
266+
}
267+
for (Expression expression : arguments) {
268+
String label = describeApiVersionResolverArgument(expression);
269+
recordVersionResolver(doc, expression, webconfigBuilder, label != null ? label : "custom ApiVersionResolver");
270+
}
271+
}
272+
273+
private void recordVersionResolver(TextDocument doc, ASTNode rangeNode, Builder webconfigBuilder, String label) {
274+
try {
275+
Range range = doc.toRange(rangeNode.getStartPosition(), rangeNode.getLength());
276+
webconfigBuilder.versionStrategy("Version Resolver: " + label, range);
277+
}
278+
catch (BadLocationException e) {
279+
}
280+
}
281+
});
282+
244283

245284
Set<String> pathConfigurerInterfaces = Set.of(
246285
Annotations.WEB_MVC_PATH_MATCH_CONFIGURER_INTERFACE,
@@ -257,6 +296,40 @@ private static Map<String, MethodInvocationExtractor> initializeMethodExtractors
257296

258297
return result;
259298
}
299+
300+
private static String describeApiVersionResolverArgument(Expression expression) {
301+
if (expression instanceof CastExpression cast) {
302+
return describeApiVersionResolverArgument(cast.getExpression());
303+
}
304+
if (expression instanceof ClassInstanceCreation cic) {
305+
ITypeBinding typeBinding = cic.getType().resolveBinding();
306+
if (typeBinding != null) {
307+
return typeBinding.getQualifiedName();
308+
}
309+
}
310+
if (expression instanceof LambdaExpression) {
311+
return "lambda expression";
312+
}
313+
if (expression instanceof MethodInvocation mi) {
314+
IMethodBinding methodBinding = mi.resolveMethodBinding();
315+
if (methodBinding != null) {
316+
ITypeBinding returnType = methodBinding.getReturnType();
317+
if (returnType != null) {
318+
return returnType.getQualifiedName();
319+
}
320+
}
321+
}
322+
if (expression instanceof Name name) {
323+
IBinding binding = name.resolveBinding();
324+
if (binding instanceof IVariableBinding variableBinding) {
325+
ITypeBinding variableType = variableBinding.getType();
326+
if (variableType != null) {
327+
return variableType.getQualifiedName();
328+
}
329+
}
330+
}
331+
return null;
332+
}
260333

261334
interface MethodInvocationExtractor {
262335
Set<String> getTargetInvocationType();

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,47 @@ public void configureApiVersioning(ApiVersionConfigurer configurer) {
136136
assertEquals(0, problems.size());
137137
}
138138

139+
@Test
140+
void webConfigUsesPathSegmentAndUseVersionResolverShowsError() throws Exception {
141+
String source = """
142+
package example.demo;
143+
144+
import org.springframework.context.annotation.Configuration;
145+
import org.springframework.web.accept.ApiVersionResolver;
146+
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
147+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
148+
149+
import jakarta.servlet.http.HttpServletRequest;
150+
151+
@Configuration
152+
public class A implements WebMvcConfigurer {
153+
154+
static final class CustomResolver implements ApiVersionResolver {
155+
@Override
156+
public String resolveVersion(HttpServletRequest request) {
157+
return null;
158+
}
159+
}
160+
161+
@Override
162+
public void configureApiVersioning(ApiVersionConfigurer configurer) {
163+
configurer.useVersionResolver(new CustomResolver());
164+
configurer.usePathSegment(1);
165+
}
166+
}
167+
""";
168+
List<ReconcileProblem> problems = reconcile(() -> {
169+
return new WebApiVersionStrategyPathSegmentReconciler();
170+
}, "A.java", source, true);
171+
172+
assertEquals(1, problems.size());
173+
174+
ReconcileProblem problem = problems.get(0);
175+
176+
assertEquals(Boot4JavaProblemType.API_VERSIONING_VIA_PATH_SEGMENT_CONFIGURED_IN_COMBINATION, problem.getType());
177+
178+
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
179+
assertEquals("configurer.usePathSegment(1)", markedStr);
180+
}
181+
139182
}

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,46 @@ void testValidationRunsOnPropertyChanges() throws Exception {
210210
}
211211

212212
}
213-
213+
214+
@Test
215+
void testNoErrorWhenWebConfigUsesVersionResolver() throws Exception {
216+
String webConfigUri = directory.toPath().resolve("src/main/java/com/example/demo/apiversioning/WebConfig.java").toUri().toString();
217+
String controllerUri = directory.toPath().resolve("src/main/java/com/example/demo/apiversioning/TestController.java").toUri().toString();
218+
219+
String updatedWebConfigSource = """
220+
package com.example.demo.apiversioning;
221+
222+
import org.springframework.context.annotation.Configuration;
223+
import org.springframework.web.accept.ApiVersionResolver;
224+
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
225+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
226+
227+
import jakarta.servlet.http.HttpServletRequest;
228+
229+
@Configuration
230+
public class WebConfig implements WebMvcConfigurer {
231+
232+
static final class CustomResolver implements ApiVersionResolver {
233+
@Override
234+
public String resolveVersion(HttpServletRequest request) {
235+
return null;
236+
}
237+
}
238+
239+
@Override
240+
public void configureApiVersioning(ApiVersionConfigurer configurer) {
241+
configurer.useVersionResolver(new CustomResolver());
242+
}
243+
244+
}
245+
""";
246+
247+
CompletableFuture<Void> updateFuture = indexer.updateDocument(webConfigUri, updatedWebConfigSource, "useVersionResolver test");
248+
updateFuture.get(5, TimeUnit.SECONDS);
249+
250+
PublishDiagnosticsParams diagnosticsResult = harness.getDiagnostics(controllerUri);
251+
List<Diagnostic> diagnostics = diagnosticsResult.getDiagnostics();
252+
assertEquals(0, diagnostics.size());
253+
}
254+
214255
}

0 commit comments

Comments
 (0)