Skip to content

Commit ea227d7

Browse files
committed
GH-1581: added new validation for types of classpath resource injections
1 parent 22af0b2 commit ea227d7

File tree

7 files changed

+644
-2
lines changed

7 files changed

+644
-2
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
@@ -40,6 +40,7 @@
4040
import org.springframework.ide.vscode.boot.java.reconcilers.BeanPostProcessingIgnoreInAotReconciler;
4141
import org.springframework.ide.vscode.boot.java.reconcilers.BeanRegistrarDeclarationReconciler;
4242
import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler;
43+
import org.springframework.ide.vscode.boot.java.reconcilers.ClasspathResourceTypeReconciler;
4344
import org.springframework.ide.vscode.boot.java.reconcilers.EntityIdForRepoReconciler;
4445
import org.springframework.ide.vscode.boot.java.reconcilers.FeignClientReconciler;
4546
import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler;
@@ -168,6 +169,10 @@ public class JdtConfig {
168169
return new EntityIdForRepoReconciler();
169170
}
170171

172+
@Bean ClasspathResourceTypeReconciler classpathResourceTypeReconciler() {
173+
return new ClasspathResourceTypeReconciler();
174+
}
175+
171176
@Conditional(LspClient.OnNotEclipseClient.class)
172177
@Bean JavaSemanticTokensProvider javaSemanticTokens() {
173178
return new JavaSemanticTokensProvider();

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public enum Boot2JavaProblemType implements ProblemType {
3434
HTTP_SECURITY_AUTHORIZE_HTTP_REQUESTS(WARNING, "'HttpSecurity.authroizeRequests(...)' API and related classes are to be deprecated use new `authorizeHttpRequests(...) and related classes", "Usage of old 'HttpSecurity.authroizeRequests(...)' API"),
3535
WEB_SECURITY_CONFIGURER_ADAPTER(WARNING, "'WebSecurityConfigurerAdapter' is removed in Spring-Security 6.x. Refactor classes extending the 'WebSecurityConfigurerAdapter' into 'Configuration' beans and methods into 'Bean' definitions ", "Replace usage of 'WebSecurityConfigurerAdapter' as this class to be removed in Security 6.x"),
3636
DOMAIN_ID_FOR_REPOSITORY(ERROR, "Invalid Domain ID type for Spring Data Repository", "Invalid Domain ID Type for Spring Data Repository"),
37-
WEB_ANNOTATION_NAMES(HINT, "Web annotation names are unnecessary when it is the same as method parameter name", "Implicit web annotations names", List.of(DiagnosticTag.Unnecessary));
37+
WEB_ANNOTATION_NAMES(HINT, "Web annotation names are unnecessary when it is the same as method parameter name", "Implicit web annotations names", List.of(DiagnosticTag.Unnecessary)),
38+
VALUE_CLASSPATH_RESOURCE_TYPE(WARNING, "Type is not compatible with classpath resource injection", "Invalid type for classpath resource in `@Value`");
3839

3940
private final ProblemSeverity defaultSeverity;
4041
private final String description;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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.Set;
17+
18+
import org.eclipse.jdt.core.dom.ASTVisitor;
19+
import org.eclipse.jdt.core.dom.Annotation;
20+
import org.eclipse.jdt.core.dom.CompilationUnit;
21+
import org.eclipse.jdt.core.dom.FieldDeclaration;
22+
import org.eclipse.jdt.core.dom.ITypeBinding;
23+
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
24+
import org.springframework.ide.vscode.boot.java.Annotations;
25+
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
26+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
27+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
28+
import org.springframework.ide.vscode.commons.java.IJavaProject;
29+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
30+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
31+
32+
/**
33+
* Validates that @Value annotations with "classpath:" or "classpath*:" prefixes
34+
* are used with compatible types that can receive Spring Resource objects.
35+
*
36+
* Valid types for classpath resources include:
37+
* - org.springframework.core.io.Resource (and Resource[])
38+
* - java.io.InputStream (and InputStream[])
39+
* - java.io.File (and File[])
40+
* - java.net.URL (and URL[])
41+
* - java.nio.file.Path (and Path[])
42+
*
43+
* Note: Array types of any valid resource type are also accepted.
44+
*
45+
* @author Martin Lippert
46+
*/
47+
public class ClasspathResourceTypeReconciler implements JdtAstReconciler {
48+
49+
// Valid types for classpath resource injection
50+
private static final Set<String> VALID_RESOURCE_TYPES = Set.of(
51+
"org.springframework.core.io.Resource",
52+
"org.springframework.core.io.Resource[]",
53+
"java.io.InputStream",
54+
"java.io.File",
55+
"java.net.URL",
56+
"java.nio.file.Path"
57+
);
58+
59+
@Override
60+
public boolean isApplicable(IJavaProject project) {
61+
// Apply to all Spring Boot 2.0+ projects
62+
return springBootVersionGreaterOrEqual(2, 0, 0).test(project);
63+
}
64+
65+
@Override
66+
public ProblemType getProblemType() {
67+
return Boot2JavaProblemType.VALUE_CLASSPATH_RESOURCE_TYPE;
68+
}
69+
70+
@Override
71+
public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, ReconcilingContext context) {
72+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu);
73+
74+
return new ASTVisitor() {
75+
76+
@Override
77+
public boolean visit(FieldDeclaration fieldDecl) {
78+
// Check annotations on the field
79+
for (Annotation annotation : ASTUtils.getAnnotations(fieldDecl)) {
80+
checkValueAnnotation(annotation, annotationHierarchies, fieldDecl.getType().resolveBinding(), context);
81+
}
82+
return super.visit(fieldDecl);
83+
}
84+
85+
@Override
86+
public boolean visit(SingleVariableDeclaration param) {
87+
ITypeBinding paramType = param.getType().resolveBinding();
88+
for (Annotation annotation : ASTUtils.getAnnotations(param)) {
89+
checkValueAnnotation(annotation, annotationHierarchies, paramType, context);
90+
}
91+
return super.visit(param);
92+
}
93+
94+
private void checkValueAnnotation(Annotation annotation, AnnotationHierarchies hierarchies,
95+
ITypeBinding typeBinding, ReconcilingContext context) {
96+
97+
// Check if it's a @Value annotation
98+
if (!hierarchies.isAnnotatedWith(annotation.resolveTypeBinding(), Annotations.VALUE)) {
99+
return;
100+
}
101+
102+
// Extract the value string from the annotation
103+
String valueString = extractValueString(annotation);
104+
if (valueString == null || valueString.isEmpty()) {
105+
return;
106+
}
107+
108+
// Check if the value starts with "classpath:" or "classpath*:"
109+
if (!isClasspathResource(valueString)) {
110+
return;
111+
}
112+
113+
// Validate the type binding
114+
if (typeBinding == null) {
115+
return;
116+
}
117+
118+
if (!isValidResourceType(typeBinding)) {
119+
String typeName = typeBinding.getQualifiedName();
120+
String message = String.format(
121+
"Type '%s' is not compatible with classpath resource injection. " +
122+
"Use org.springframework.core.io.Resource, java.io.InputStream, java.io.File, " +
123+
"java.net.URL, or java.nio.file.Path",
124+
typeName
125+
);
126+
127+
ReconcileProblemImpl problem = new ReconcileProblemImpl(
128+
getProblemType(),
129+
message,
130+
annotation.getStartPosition(),
131+
annotation.getLength()
132+
);
133+
context.getProblemCollector().accept(problem);
134+
}
135+
}
136+
137+
private String extractValueString(Annotation annotation) {
138+
// getAttribute handles both @Value("...") and @Value(value="...") cases
139+
return ASTUtils.getAttribute(annotation, "value")
140+
.map(expr -> ASTUtils.getExpressionValueAsString(expr, v -> {}))
141+
.orElse(null);
142+
}
143+
144+
private boolean isClasspathResource(String value) {
145+
// Remove ${} placeholder wrappers to check the actual value
146+
String trimmed = value.trim();
147+
if (trimmed.startsWith("${") && trimmed.endsWith("}")) {
148+
trimmed = trimmed.substring(2, trimmed.length() - 1).trim();
149+
}
150+
151+
return trimmed.startsWith("classpath:") || trimmed.startsWith("classpath*:");
152+
}
153+
154+
private boolean isValidResourceType(ITypeBinding typeBinding) {
155+
String qualifiedName = typeBinding.getQualifiedName();
156+
157+
// Direct match with valid types
158+
if (VALID_RESOURCE_TYPES.contains(qualifiedName)) {
159+
return true;
160+
}
161+
162+
// Check for array types - arrays of any valid resource type are also valid
163+
if (typeBinding.isArray()) {
164+
ITypeBinding elementType = typeBinding.getElementType();
165+
if (elementType != null) {
166+
String elementTypeName = elementType.getQualifiedName();
167+
// Check if the element type is one of the valid resource types
168+
if (VALID_RESOURCE_TYPES.contains(elementTypeName)) {
169+
return true;
170+
}
171+
}
172+
}
173+
174+
return false;
175+
}
176+
};
177+
}
178+
}
179+

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024 Broadcom
2+
* Copyright (c) 2024, 2025 Broadcom
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -26,6 +26,7 @@
2626
import org.eclipse.jdt.core.dom.SimpleName;
2727
import org.eclipse.jdt.core.dom.SimpleType;
2828
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
29+
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
2930
import org.eclipse.jdt.core.dom.TypeDeclaration;
3031

3132
public class CompositeASTVisitor extends ASTVisitor {
@@ -105,6 +106,18 @@ public boolean visit(SingleMemberAnnotation node) {
105106
return result;
106107
}
107108

109+
@Override
110+
public boolean visit(SingleVariableDeclaration node) {
111+
boolean result = true;
112+
if (!checkOffset(node)) {
113+
return false;
114+
}
115+
for (ASTVisitor astVisitor : visitors) {
116+
result |= astVisitor.visit(node);
117+
}
118+
return result;
119+
}
120+
108121
@Override
109122
public boolean visit(NormalAnnotation node) {
110123
boolean result = true;

headless-services/spring-boot-language-server/src/main/resources/problem-types.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@
9191
"label": "Implicit web annotations names",
9292
"description": "Web annotation names are unnecessary when it is the same as method parameter name",
9393
"defaultSeverity": "HINT"
94+
},
95+
{
96+
"code": "VALUE_CLASSPATH_RESOURCE_TYPE",
97+
"label": "Invalid type for classpath resource in `@Value`",
98+
"description": "Type is not compatible with classpath resource injection",
99+
"defaultSeverity": "WARNING"
94100
}
95101
]
96102
},

0 commit comments

Comments
 (0)