Skip to content

Commit 3f1a2ed

Browse files
firend6334-hueАлексей Яхненко
authored andcommitted
Add @AcceptableExtension validation for file uploads
Signed-off-by: Алексей Яхненко <firend6334@gmail.com>
1 parent 386c6ca commit 3f1a2ed

File tree

4 files changed

+633
-0
lines changed

4 files changed

+633
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.bind.annotation;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
23+
24+
/**
25+
* Annotation for validating file extensions of multipart file uploads in Spring MVC
26+
* controller methods. When applied to a {@link org.springframework.web.multipart.MultipartFile}
27+
* parameter, it restricts the acceptable file extensions that can be uploaded.
28+
*
29+
* <p>This annotation works in conjunction with a custom argument resolver or validator
30+
* to enforce file extension constraints at the controller level, providing early
31+
* validation before file processing.
32+
*
33+
* <p>Example usage:
34+
* <pre class="code">
35+
* &#064;PostMapping("/upload")
36+
* public String handleFileUpload(
37+
* &#064;AcceptableExtension(extensions = {"jpg", "png", "pdf"})
38+
* &#064;RequestParam("file") MultipartFile file) {
39+
* // Process file
40+
* return "success";
41+
* }
42+
* </pre>
43+
*
44+
* @author Aleksei Iakhnenko
45+
* @since 7.0
46+
* @see org.springframework.web.multipart.MultipartFile
47+
* @see org.springframework.web.bind.annotation.RequestParam
48+
*/
49+
@Target(ElementType.PARAMETER)
50+
@Retention(RetentionPolicy.RUNTIME)
51+
public @interface AcceptableExtension {
52+
String[] extensions() default {};
53+
String message() default "Invalid file extension";
54+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.method.annotation;
18+
19+
import java.util.Arrays;
20+
21+
import jakarta.servlet.http.HttpServletRequest;
22+
import org.jspecify.annotations.Nullable;
23+
24+
import org.springframework.core.MethodParameter;
25+
import org.springframework.util.StringUtils;
26+
import org.springframework.web.bind.annotation.AcceptableExtension;
27+
import org.springframework.web.bind.support.WebDataBinderFactory;
28+
import org.springframework.web.context.request.NativeWebRequest;
29+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
30+
import org.springframework.web.method.support.ModelAndViewContainer;
31+
import org.springframework.web.multipart.MultipartException;
32+
import org.springframework.web.multipart.MultipartFile;
33+
import org.springframework.web.multipart.support.MultipartResolutionDelegate;
34+
35+
/**
36+
* Resolves method arguments annotated with @AcceptableExtension and validates
37+
* file extensions for MultipartFile parameters.
38+
*
39+
* @author Aleksei Iakhnenko
40+
* @since 7.0
41+
* @see AcceptableExtension
42+
*/
43+
public class AcceptableExtensionMethodArgumentResolver implements HandlerMethodArgumentResolver {
44+
45+
@Override
46+
public boolean supportsParameter(MethodParameter parameter) {
47+
return parameter.hasParameterAnnotation(AcceptableExtension.class);
48+
}
49+
50+
@Override
51+
@Nullable
52+
public Object resolveArgument(
53+
MethodParameter parameter,
54+
@Nullable ModelAndViewContainer mavContainer,
55+
NativeWebRequest webRequest,
56+
@Nullable WebDataBinderFactory binderFactory) throws Exception {
57+
58+
AcceptableExtension annotation = parameter.getParameterAnnotation(AcceptableExtension.class);
59+
if (annotation == null) {
60+
return null;
61+
}
62+
63+
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
64+
if (servletRequest == null) {
65+
return null;
66+
}
67+
68+
String paramName = getParameterName(parameter);
69+
if (paramName == null) {
70+
return null;
71+
}
72+
73+
Object resolvedArgument = MultipartResolutionDelegate.resolveMultipartArgument(
74+
paramName, parameter, servletRequest);
75+
76+
MultipartFile file = (resolvedArgument instanceof MultipartFile) ?
77+
(MultipartFile) resolvedArgument :
78+
null;
79+
80+
if (file != null && !file.isEmpty()) {
81+
String filename = file.getOriginalFilename();
82+
if (StringUtils.hasText(filename)) {
83+
String extension = StringUtils.getFilenameExtension(filename);
84+
if (extension != null && !isAcceptableExtension(extension, annotation.extensions())) {
85+
throw new MultipartException(annotation.message() +
86+
". Allowed: " + Arrays.toString(annotation.extensions()) +
87+
", received: " + extension);
88+
}
89+
}
90+
}
91+
92+
return file;
93+
}
94+
95+
/**
96+
* Determine the name for the given method parameter.
97+
* @param parameter the method parameter
98+
* @return the parameter name, or {@code null} if not resolvable
99+
*/
100+
@Nullable
101+
private String getParameterName(MethodParameter parameter) {
102+
org.springframework.web.bind.annotation.RequestParam requestParam =
103+
parameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestParam.class);
104+
105+
if (requestParam != null) {
106+
String paramName = requestParam.value();
107+
if (StringUtils.hasText(paramName)) {
108+
return paramName;
109+
}
110+
paramName = requestParam.name();
111+
if (StringUtils.hasText(paramName)) {
112+
return paramName;
113+
}
114+
}
115+
116+
// Fallback to actual parameter name if available
117+
return parameter.getParameterName();
118+
}
119+
120+
private boolean isAcceptableExtension(String extension, String[] acceptableExtensions) {
121+
if (acceptableExtensions.length == 0) {
122+
return true;
123+
}
124+
return Arrays.stream(acceptableExtensions)
125+
.anyMatch(acceptable -> acceptable.equalsIgnoreCase(extension));
126+
}
127+
128+
}

0 commit comments

Comments
 (0)