Chapter 3. File Upload and Error Handling

In this chapter, we will enable our user to upload a profile picture. We will also see how to handle errors in Spring MVC.

Uploading a file

We will now make it possible for our user to upload a profile picture. This will be available from the profile page later on, but for now, we will simplify things and create a new page in the templates directory under profile/uploadPage.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorator="layout/default">
<head lang="en">
    <title>Profile Picture Upload</title>
</head>
<body>
<div class="row" layout:fragment="content">

    <h2 class="indigo-text center">Upload</h2>

    <form th:action="@{/upload}" method="post" enctype="multipart/form-data" class="col m8 s12 offset-m2">

        <div class="input-field col s6">
            <input type="file" id="file" name="file"/>
        </div>

        <div class="col s6 center">
            <button class="btn indigo waves-effect waves-light" type="submit" name="save" th:text="#{submit}">Submit
                <i class="mdi-content-send right"></i>
            </button>
        </div>
    </form>
</div>
</body>
</html>

Not much to see besides the enctype attribute on the form. The file will be sent by the POST method to the upload URL. We will now create the corresponding controller right beside ProfileController in the profile package:

package masterSpringMvc.profile;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class PictureUploadController {
    public static final Resource PICTURES_DIR = new FileSystemResource("./pictures");

    @RequestMapping("upload")
    public String uploadPage() {
        return "profile/uploadPage";
    }

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String onUpload(MultipartFile file) throws IOException {
        String filename = file.getOriginalFilename();
        File tempFile = File.createTempFile("pic", getFileExtension(filename), PICTURES_DIR.getFile());

        try (InputStream in = file.getInputStream();
             OutputStream out = new FileOutputStream(tempFile)) {
            IOUtils.copy(in, out);
        }

        return "profile/uploadPage";
    }

    private static String getFileExtension(String name) {
        return name.substring(name.lastIndexOf("."));
    }
}

The first thing this code will do is create a temporary file in the pictures directory, which can be found inside the project's root folder; so, ensure that it exists. In Java, a temporary file is just a commodity to obtain a unique file identifier on the filesystem. It is up to the user to optionally delete it.

Create a pictures directory at the root of the project and add an empty file called .gitkeep to ensure that you can commit it in Git.

Tip

Empty directories in Git

Git is file-based and it is not possible to commit an empty directory. A common workaround is to commit an empty file, such as .gitkeep, in a directory to force Git to keep it under version control.

The file uploaded by the user will be injected as a MultipartFile interface in our controller. This interface provides several methods to get the name of the file, its size, and its contents.

The method that particularly interests us here is getInputStream(). We will indeed copy this stream to a fileOutputStream method, thanks to the IOUtils.copy method. The code to write an input stream to an output stream is pretty boring, so it's handy to have the Apache Utils in the classpath (it is part of the tomcat-embedded-core.jar file).

We make heavy use of the pretty cool Spring and Java 7 NIO features:

  • The resource class of string is a utility class that represents an abstraction of resources that can be found in different ways
  • The try…with block will automatically close our streams even in the case of an exception, removing the boilerplate of writing a finally block

With the preceding code, any file uploaded by the user will be copied into the pictures directory.

There are a handful of properties available in Spring Boot to customize file upload. Take a look at the MultipartProperties class.

The most interesting ones are:

  • multipart.maxFileSize: This defines the maximum file size allowed for the uploaded files. Trying to upload a bigger one will result in a MultipartException class. The default value is 1Mb.
  • multipart.maxRequestSize: This defines the maximum size of the multipart request. The default value is 10Mb.

The defaults are good enough for our application. After a few uploads, our picture directory will look like this:

Uploading a file

Wait! Somebody uploaded a ZIP file! I cannot believe it. We better add some checks in our controller to ensure that the uploaded files are real images:

package masterSpringMvc.profile;

import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.io.*;

@Controller
public class PictureUploadController {
    public static final Resource PICTURES_DIR = new FileSystemResource("./pictures");

    @RequestMapping("upload")
    public String uploadPage() {
        return "profile/uploadPage";
    }

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String onUpload(MultipartFile file, RedirectAttributes redirectAttrs) throws IOException {

        if (file.isEmpty() || !isImage(file)) {
            redirectAttrs.addFlashAttribute("error", "Incorrect file. Please upload a picture.");
            return "redirect:/upload";
        }

        copyFileToPictures(file);

        return "profile/uploadPage";
    }

    private Resource copyFileToPictures(MultipartFile file) throws IOException {
        String fileExtension = getFileExtension(file.getOriginalFilename());
        File tempFile = File.createTempFile("pic", fileExtension, PICTURES_DIR.getFile());
        try (InputStream in = file.getInputStream();
             OutputStream out = new FileOutputStream(tempFile)) {

            IOUtils.copy(in, out);
        }
        return new FileSystemResource(tempFile);
    }

    private boolean isImage(MultipartFile file) {
        return file.getContentType().startsWith("image");
    }

    private static String getFileExtension(String name) {
        return name.substring(name.lastIndexOf("."));
    }
}

Pretty easy! The getContentType() method returns the Multipurpose Internet Mail Extensions (MIME) type of the file. It will be image/png, image/jpg, and so on. So we just have to check if the MIME type starts with "image".

We added an error message to the form so we should add something in our web page to display it. Place the following code just under the title in the uploadPage:

<div class="col s12 center red-text" th:text="${error}" th:if="${error}">
    Error during upload
</div>

The next time you try to upload a ZIP file, you will get an error! This is shown in the following screenshot:

Uploading a file

Writing an image to the response

The uploaded images are not served from the static directories. We will need to take special measures to display them in our web page.

Let's add the following lines to our upload page, just above the form:

<div class="col m8 s12 offset-m2">
    <img th:src="@{/uploadedPicture}" width="100" height="100"/>
</div>

This will try and get the image from our controller. Let's add the corresponding method to the PictureUploadController class:


@RequestMapping(value = "/uploadedPicture")
public void getUploadedPicture(HttpServletResponse response) throws IOException {
    ClassPathResource classPathResource = new ClassPathResource("/images/anonymous.png");
    response.setHeader("Content-Type", URLConnection.guessContentTypeFromName(classPathResource.getFilename()));
    IOUtils.copy(classPathResource.getInputStream(), response.getOutputStream());
}

This code will write an image found in the src/main/resources/images/anonymous.png directory directly to the response! How exciting!

If we go to our page again, we will see the following image:

Writing an image to the response

Tip

I found the anonymous user image on iconmonstr (http://iconmonstr.com/user-icon) and downloaded it as a 128 x 128 PNG file.

Managing upload properties

A good thing to do at this point is to allow the configuration of the upload directory and the path to the anonymous user image through the application.properties file.

Let's create a PicturesUploadProperties class inside a newly created config package:

package masterSpringMvc.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;

import java.io.IOException;
@ConfigurationProperties(prefix = "upload.pictures")
public class PicturesUploadProperties {
    private Resource uploadPath;
    private Resource anonymousPicture;

    public Resource getAnonymousPicture() {
        return anonymousPicture;
    }

    public void setAnonymousPicture(String anonymousPicture) {
        this.anonymousPicture = new DefaultResourceLoader().getResource(anonymousPicture);
    }

    public Resource getUploadPath() {
        return uploadPath;
    }

    public void setUploadPath(String uploadPath) {
        this.uploadPath = new DefaultResourceLoader().getResource(uploadPath);
    }
}

In this class, we make use of the Spring Boot ConfigurationProperties. This will tell Spring Boot to automatically map properties found in the classpath (by default, in the application.properties file) in a type-safe fashion.

Notice that we defined setters taking 'String's as arguments but are at liberty to let the getters return any type is the most useful.

We now need to add the PicturesUploadProperties class to our configuration:

@SpringBootApplication
@EnableConfigurationProperties({PictureUploadProperties.class})
public class MasterSpringMvc4Application extends WebMvcConfigurerAdapter {
  // code omitted
}

We can now add the properties' values inside the application.properties file:

upload.pictures.uploadPath=file:./pictures
upload.pictures.anonymousPicture=classpath:/images/anonymous.png

Because we use Spring's DefaultResourceLoader class, we can use prefixes such as file: or classpath: to specify where our resources can be found.

This would be the equivalent of creating a FileSystemResource class or a ClassPathResource class.

This approach also has the advantage of documenting the code. We can easily see that the picture directory will be found in the application root, whereas the anonymous picture will be found in the classpath.

That's it. We can now use our properties inside our controller. The following are the relevant parts of the PictureUploadController class:

package masterSpringMvc.profile;

import masterSpringMvc.config.PictureUploadProperties;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLConnection;

@Controller
public class PictureUploadController {
    private final Resource picturesDir;
    private final Resource anonymousPicture;

    @Autowired
    public PictureUploadController(PictureUploadProperties uploadProperties) {
        picturesDir = uploadProperties.getUploadPath();
        anonymousPicture = uploadProperties.getAnonymousPicture();
    }

    @RequestMapping(value = "/uploadedPicture")
    public void getUploadedPicture(HttpServletResponse response) throws IOException {
        response.setHeader("Content-Type", URLConnection.guessContentTypeFromName(anonymousPicture.getFilename()));
        IOUtils.copy(anonymousPicture.getInputStream(), response.getOutputStream());
    }

  private Resource copyFileToPictures(MultipartFile file) throws IOException {
       String fileExtension = getFileExtension(file.getOriginalFilename());
       File tempFile = File.createTempFile("pic", fileExtension, picturesDir.getFile());
       try (InputStream in = file.getInputStream();
            OutputStream out = new FileOutputStream(tempFile)) {

           IOUtils.copy(in, out);
       }
       return new FileSystemResource(tempFile);
   }    
// The rest of the code remains the same
}

At this point, if you launch your application again, you will see that the result hasn't changed. The anonymous picture is still displayed and the pictures uploaded by our users still end up in the pictures directory at the project root.

Displaying the uploaded picture

It would be nice to display the user's picture now, wouldn't it? To do this, we will add a model attribute to our PictureUploadController class:

@ModelAttribute("picturePath")
public Resource picturePath() {
  return anonymousPicture;
}

We can now inject it to retrieve its value when we serve the uploaded picture:

@RequestMapping(value = "/uploadedPicture")
public void getUploadedPicture(HttpServletResponse response, @ModelAttribute("picturePath") Path picturePath) throws IOException {
    response.setHeader("Content-Type", URLConnection.guessContentTypeFromName(picturePath.toString()));
    Files.copy(picturePath, response.getOutputStream());
}

The @ModelAttribute annotation is a handy way to create model attributes with an annotated method. They can then be injected with the same annotation into controller methods. With this code, a picturePath parameter will be available in the model as long as we are not redirected to another page. Its default value is the anonymous picture we defined in our properties.

We need to update this value when the file is uploaded. Update the onUpload method:

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String onUpload(MultipartFile file, RedirectAttributes redirectAttrs, Model model) throws IOException {

    if (file.isEmpty() || !isImage(file)) {
        redirectAttrs.addFlashAttribute("error", "Incorrect file. Please upload a picture.");
        return "redirect:/upload";
    }

    Resource picturePath = copyFileToPictures(file);
    model.addAttribute("picturePath", picturePath);

    return "profile/uploadPage";
}

By injecting the model, we can update the picturePath parameter after the upload is complete.

Now, the problem is that our two methods, onUpload and getUploadedPicture, will occur in different requests. Unfortunately, the model attributes will be reset between each.

That's why we will define the picturePath parameter as a session attribute. We can do this by adding another annotation to our controller class:

@Controller
@SessionAttributes("picturePath")
public class PictureUploadController {
}

Phew! That's a lot of annotations just to handle a simple session attribute. You will get the following output:

Displaying the uploaded picture

This approach makes code composition really easy. Plus, we didn't use HttpServletRequest or HttpSession directly. Moreover, our object can be typed easily.

Handling file upload errors

It must have certainly occurred to my attentive readers that our code is susceptible to throw two kinds of exceptions:

  • IOException: This error is thrown if something bad happens while writing the file to disk.
  • MultipartException: This error is thrown if an error occurs while uploading the file. For instance, when the maximum file size is exceeded.

This will give us a good opportunity to look at two ways of handling exceptions in Spring:

  • Using the @ExceptionHandler annotation locally in a controller method
  • Using a global exception handler defined at the Servlet container level

Let's handle IOException with the @ExceptionHandler annotation inside our PictureUploadController class by adding the following method:

@ExceptionHandler(IOException.class)
public ModelAndView handleIOException(IOException exception) {
    ModelAndView modelAndView = new ModelAndView("profile/uploadPage");
    modelAndView.addObject("error", exception.getMessage());
    return modelAndView;
}

This is a simple yet powerful approach. This method will be called every time an IOException is thrown in our controller.

In order to test the exception handler, since making the Java IO code throw an exception can be tricky, just replace the onUpload method body during the test:

@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String onUpload(MultipartFile file, RedirectAttributes redirectAttrs, Model model) throws IOException {
    throw new IOException("Some message");
}

After this change, if we try to upload a picture, we will see the error message of this exception displayed on the upload page:

Handling file upload errors

Now, we will handle the MultipartException. This needs to happen at the Servlet container level (that is, at the Tomcat level), as this exception is not thrown directly by our controller.

We will need to add a new EmbeddedServletContainerCustomizer bean to our configuration. Add this method to the WebConfiguration class:

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
    EmbeddedServletContainerCustomizer 
embeddedServletContainerCustomizer = new EmbeddedServletContainerCustomizer() {
        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            container.addErrorPages(new ErrorPage(MultipartException.class, "/uploadError"));
        }
    };
    return embeddedServletContainerCustomizer;
}

This is a little verbose. Note that EmbeddedServletContainerCustomizer is an interface that contains a single method; it can therefore be replaced by a lambda expression:

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
    EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer
            = container -> container.addErrorPages(new ErrorPage(MultipartException.class, "/uploadError"));
    return embeddedServletContainerCustomizer;
}

So, let's just write the following:

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
    return container -> container.addErrorPages(new ErrorPage(MultipartException.class, "/uploadError"));
}

This code creates a new error page, which will be called when a MultipartException happens. It can also be mapped to an HTTP status. The EmbeddedServletContainerCustomizer interface has many other features that will allow the customization of the Servlet container in which our application runs. Visit http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-web-applications.html#boot-features-customizing-embedded-containers for more information.

We now need to handle this uploadError URL in our PictureUploadController class:

@RequestMapping("uploadError")
public ModelAndView onUploadError(HttpServletRequest request) {
    ModelAndView modelAndView = new ModelAndView("uploadPage");
    modelAndView.addObject("error", request.getAttribute(WebUtils.ERROR_MESSAGE_ATTRIBUTE));
    return modelAndView;
}

The error pages defined in a Servlet environment contain a number of interesting attributes that will help debug the error:

Attribute

Description

javax.servlet.error.status_code

This is the HTTP status code of the error.

javax.servlet.error.exception_type

This is the exception class.

javax.servlet.error.message

This is the message of the exception thrown.

javax.servlet.error.request_uri

This is the URI on which the exception occurred.

javax.servlet.error.exception

This is the actual exception.

javax.servlet.error.servlet_name

This is the name of the Servlet that caught the exception.

All these attributes are conveniently accessible on the WebUtils class of Spring Web.

If someone tries to upload too big a file, they will get a very clear error message.

You can now test that the error is handled correctly by uploading a really big file (> 1Mb) or setting the multipart.maxFileSize property to a lower value: 1kb for instance:

Handling file upload errors
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset