Recently on my Spring 3 MVC project, I needed to implement file upload/download capability - nothing too exciting here, but I wanted to use JSR-303 and have my controller to cleanly check for a valid file type and size (my project only allows for uploading PDF files with 30Mb max), that is to move that constraint logic away from the controller into a JSR-303 validator.
Since I'm a nice guy, I'm sharing the code here :)
here is an excerpt of my controller code:
1: import org.slf4j.Logger;
2: import org.springframework.beans.factory.annotation.Autowired;
3: import org.springframework.security.core.context.SecurityContextHolder;
4: import org.springframework.stereotype.Controller;
5: import org.springframework.ui.Model;
6: import org.springframework.validation.BindingResult;
7: import org.springframework.web.bind.annotation.ExceptionHandler;
8: import org.springframework.web.bind.annotation.RequestMapping;
9: import org.springframework.web.bind.annotation.RequestMethod;
10: import javax.validation.Valid;
11: import static org.slf4j.LoggerFactory.getLogger;
12: @Controller
13: @RequestMapping(value = "/upload")
14: public class UploadController
15: {
16: private static final Logger LOGGER = getLogger(UploadController.class);
17: @Autowired
18: private ProjectManagementService projectManagementService;
19: @RequestMapping(method = RequestMethod.GET)
20: public String getUploadForm(Model model)
21: {
22: String viewName = "upload/uploadForm";
23: User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
24: if (user.getAuthorities().contains(Authority.ROLE_ADMIN))
25: {
26: viewName = "redirect:admin/adminAccessList";
27: }
28: else if (user.getAuthorities().contains(Authority.ROLE_USER))
29: {
30: model.addAttribute(new ProjectUploadCommand());
31: }
32: else
33: {
34: throw new IllegalStateException("Unauthorized access - should never happen.");
35: }
36: return viewName;
37: }
38: @RequestMapping(method = RequestMethod.POST)
39: public String upload(@Valid ProjectUploadCommand projectUpload, BindingResult result)
40: {
41: String viewName = "upload/uploadForm";
42: if (!result.hasErrors())
43: {
44: final User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
45: Project project = new Project(user,
46: projectUpload.getProjectIdeaTitle(),
47: projectUpload.getDescription(),
48: projectUpload.getEstimatedLaunchDate(),
49: new ProjectContent(projectUpload.getFileData().getOriginalFilename(), projectUpload.getFileData().getBytes()));
50: projectManagementService.addProject(user, project);
51: viewName = "upload/thankYou";
52: }
53: return viewName;
54: }
55: @ExceptionHandler
56: public String catchAllHandler(Exception ex)
57: {
58: LOGGER.error("Unexpected Exception at UploadController", ex);
59: return "errorPage";
60: }
61: }
Note the @Valid annotation in front of ProjectUploadCommand object.
(I know, a better name for it would be ProjectUploadForm since its not a real command in the GOF sense, but its how some Spring folks call these objects)
here is the "command" object:
CommonsMultipartFileValid.java
1: import java.lang.annotation.Documented;
2: import java.lang.annotation.Retention;
3: import java.lang.annotation.RetentionPolicy;
4: import java.lang.annotation.Target;
5: import javax.validation.Constraint;
6: import javax.validation.Payload;
7: import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
8: import static java.lang.annotation.ElementType.CONSTRUCTOR;
9: import static java.lang.annotation.ElementType.FIELD;
10: import static java.lang.annotation.ElementType.METHOD;
11: import static java.lang.annotation.ElementType.PARAMETER;
12: @SuppressWarnings({"UnusedDeclaration"})
13: @Target({ANNOTATION_TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR})
14: @Retention(RetentionPolicy.RUNTIME)
15: @Constraint(validatedBy = CommonsMultipartFileValidator.class)
16: @Documented
17: public @interface CommonsMultipartFileValid
18: {
19: String fileSize() default "31457280";
20: String[] supportedFileTypes() default {"pdf"};
21: String message() default "File must be a PDF no larger then 30M bytes";
22: Class[] groups() default {};
23: Class[] payload() default {};
24: }
and here is the implementation: (left a TO DO for you !)
CommonsMultipartFileValidator.java
1: import java.util.Arrays;
2: import java.util.List;
3: import javax.validation.ConstraintValidator;
4: import javax.validation.ConstraintValidatorContext;
5: import org.springframework.web.multipart.commons.CommonsMultipartFile;
6: /**
7: * A custom JSR-303 validator for CommonsMultipartFile objects.
8: * validates the file extension and file size.
9: *
10: * @author Paulo Avelar
11: */
12: public class CommonsMultipartFileValidator implements ConstraintValidator<commonsmultipartfilevalid commonsmultipartfile="commonsmultipartfile">
13: {
14: private String fileSize = "31457280"; //30M-bytes
15: private String[] supportedFileTypes = {"PDF"};
16: @Override
17: public void initialize(CommonsMultipartFileValid constraintAnnotation)
18: {
19: fileSize = constraintAnnotation.fileSize();
20: supportedFileTypes = constraintAnnotation.supportedFileTypes();
21: }
22: @Override
23: public boolean isValid(CommonsMultipartFile value, ConstraintValidatorContext context)
24: {
25: boolean result = false;
26: try
27: {
28: if (value != null)
29: {
30: result = validateFileSize(value);
31: //two step process because MIME type identification can be costly
32: if (result)
33: {
34: result = validateExtensionType(value);
35: }
36: }
37: }
38: catch (Exception e)
39: {
40: throw new RuntimeException(e);
41: }
42: return result;
43: }
44: // TODO: use mime type identifier instead of silly extension name
45: private boolean validateExtensionType(CommonsMultipartFile value)
46: {
47: int dotPos = value.getOriginalFilename().lastIndexOf(".");
48: boolean result = false;
49: if (dotPos != -1)
50: {
51: String extension = value.getOriginalFilename().substring(dotPos + 1);
52: final List<string> supportedExtensions = Arrays.asList(supportedFileTypes);
53: for (String supportedExtension : supportedExtensions)
54: {
55: if (extension.equalsIgnoreCase(supportedExtension))
56: {
57: result = true;
58: break;
59: }
60: }
61: }
62: return result;
63: }
64: private boolean validateFileSize(CommonsMultipartFile value)
65: {
66: boolean result = false;
67: if (value != null)
68: {
69: if (value.getSize() != 0 && value.getSize() <= Long.valueOf(fileSize))
70: {
71: result = true;
72: }
73: }
74: return result;
75: }
76: }
Almost forgot, here is the Unit Test for it:
CommonsMultipartValidatorTest.java
1: import java.io.File;
2: import java.io.IOException;
3: import java.io.InputStream;
4: import java.io.OutputStream;
5: import java.io.UnsupportedEncodingException;
6: import java.lang.annotation.Annotation;
7: import javax.validation.Payload;
8: import org.apache.commons.fileupload.FileItem;
9: import org.hamcrest.Matchers;
10: import org.junit.Test;
11: import org.springframework.web.multipart.commons.CommonsMultipartFile;
12: import static org.hamcrest.MatcherAssert.assertThat;
13: public class CommonsMultipartValidatorTest
14: {
15: private CommonsMultipartFileValid commonsMultipartFileValid = new CommonsMultipartFileValid()
16: {
17: @Override
18: public String fileSize()
19: {
20: return "10000";
21: }
22: @Override
23: public String[] supportedFileTypes()
24: {
25: return new String[]{"PDF", "DOC"};
26: }
27: @Override
28: public String message()
29: {
30: return "whatever message";
31: }
32: @Override
33: public Class[] groups()
34: {
35: return null;
36: }
37: @Override
38: public Class[] payload()
39: {
40: return null;
41: }
42: @Override
43: public Class annotationType()
44: {
45: return null;
46: }
47: };
48: @Test
49: public void validateCommonsMultipartValidator()
50: {
51: CommonsMultipartFileValidator validator = new CommonsMultipartFileValidator();
52: validator.initialize(commonsMultipartFileValid);
53: assertThat(validator.isValid(new CommonsMultipartFile(new ValidFileItem()), null), Matchers.is(true));
54: assertThat(validator.isValid(new CommonsMultipartFile(new InvalidFileItem()), null), Matchers.is(false));
55: assertThat(validator.isValid(new CommonsMultipartFile(new InvalidFileItemSize()), null), Matchers.is(false));
56: }
57: private class ValidFileItem implements FileItem
58: {
59: @Override
60: public InputStream getInputStream() throws IOException
61: {
62: return null;
63: }
64: @Override
65: public String getContentType()
66: {
67: return null;
68: }
69: @Override
70: public String getName()
71: {
72: return "test.pdf";
73: }
74: @Override
75: public boolean isInMemory()
76: {
77: return false;
78: }
79: @Override
80: public long getSize()
81: {
82: return 450;
83: }
84: @Override
85: public byte[] get()
86: {
87: return new byte[0];
88: }
89: @Override
90: public String getString(String encoding) throws UnsupportedEncodingException
91: {
92: return null;
93: }
94: @Override
95: public String getString()
96: {
97: return null;
98: }
99: @Override
100: public void write(File file) throws Exception
101: {
102: }
103: @Override
104: public void delete()
105: {
106: }
107: @Override
108: public String getFieldName()
109: {
110: return null;
111: }
112: @Override
113: public void setFieldName(String name)
114: {
115: }
116: @Override
117: public boolean isFormField()
118: {
119: return false;
120: }
121: @Override
122: public void setFormField(boolean state)
123: {
124: }
125: @Override
126: public OutputStream getOutputStream() throws IOException
127: {
128: return null;
129: }
130: }
131: private class InvalidFileItem extends ValidFileItem
132: {
133: @Override
134: public String getName()
135: {
136: return "test.XXX";
137: }
138: }
139: private class InvalidFileItemSize extends ValidFileItem
140: {
141: @Override
142: public long getSize()
143: {
144: return 9999999;
145: }
146: }
147: }
This comment has been removed by a blog administrator.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDelete