Skip to content

Commit

Permalink
Refresh guide content and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bclozel committed Sep 1, 2016
1 parent a5ca5c6 commit f60289d
Show file tree
Hide file tree
Showing 24 changed files with 563 additions and 125 deletions.
49 changes: 27 additions & 22 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
---
tags: [files, upload, hateoas, mvc]
projects: [spring-framework, spring-hateoas]
---
:spring_version: current
:spring_boot_version: 1.4.0.RELEASE
:Component: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/stereotype/Component.html
:Controller: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/stereotype/Controller.html
:DispatcherServlet: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/servlet/DispatcherServlet.html
:SpringApplication: http://docs.spring.io/spring-boot/docs/{spring_boot_version}/api/org/springframework/boot/SpringApplication.html
:ResponseBody: http://docs.spring.io/spring/docs/{spring_version}/javadoc-api/org/springframework/web/bind/annotation/ResponseBody.html
:SpringBootApplication: http://docs.spring.io/spring-boot/docs/{spring_boot_version}/api/org/springframework/boot/autoconfigure/SpringBootApplication.html
Expand All @@ -15,11 +9,11 @@ projects: [spring-framework, spring-hateoas]
:source-highlighter: prettify
:project_id: gs-uploading-files

This guide walks you through the process of creating a server application that can receive multi-part file uploads.
This guide walks you through the process of creating a server application that can receive HTTP multi-part file uploads.

== What you'll build

You will create a Spring MVC application that accepts file uploads. You will also build a simple client to upload a test file.
You will create a Spring Boot web application that accepts file uploads. You will also build a simple HTML interface to upload a test file.


== What you'll need
Expand All @@ -40,44 +34,42 @@ include::https://raw.githubusercontent.com/spring-guides/getting-started-macros/


[[initial]]
== Create a configuration class
== Create an Application class

To upload files with Servlet 3.0 containers, you need to register a `MultipartConfigElement` class (which would be `<multipart-config>` in web.xml). Thanks to Spring Boot, that bean is already registered and available! All you need to get started with this application is the following, empty configuration setup.
To start a Spring Boot MVC application, we first need a starter; here, `spring-boot-starter-thymeleaf` and `spring-boot-starter-web` are already added as dependencies. To upload files with Servlet containers, you need to register a `MultipartConfigElement` class (which would be `<multipart-config>` in web.xml). Thanks to Spring Boot, everything is auto-configured for you!

All you need to get started with this application is the following `Application` class.

`src/main/java/hello/Application.java`
[source,java]
----
include::initial/src/main/java/hello/Application.java[]
----

You will soon add a Spring MVC controller, which is why you need `@SpringBootApplication`. Spring Boot automatically finds `@Controller`-marked classes and registers them with the application context.

As part of auto-configuring Spring MVC, Spring Boot will create a `MultipartConfigElement` bean and make itself ready for file uploads.

NOTE: http://tomcat.apache.org/tomcat-7.0-doc/servletapi/javax/servlet/MultipartConfigElement.html[MultipartConfigElement] is a Servlet 3.0 standard element that defines the limits on uploading files. This component is supported by all compliant containers like Tomcat and Jetty. Later on in this guide, we'll see how to configure its limits.


== Create a file upload controller

In Spring MVC, a controller is used to handle file upload requests. The following code provides the web app with the ability to upload files.
The initial application already contains a few classes to deal with storing and loading the uploaded files on disk; they're all located in the `hello.storage` package. We'll use those in our new `FileUploadController`.

`src/main/java/hello/FileUploadController.java`
[source,java]
----
include::complete/src/main/java/hello/FileUploadController.java[]
----

The entire class is marked up with `@Controller` so Spring MVC can pick it up and look for routes.
This class is annotated with `@Controller` so Spring MVC can pick it up and look for routes. Each method is tagged with `@GetMapping` or `@PostMapping` to tie the path and the HTTP action to a particular Controller action.

Each method is tagged with `@RequestMapping` to flag the path and the HTTP action. In this case, `GET` looks up the current list of uploaded files (stored in `Application.ROOT` folder) and loads it into a Thymeleaf template. It provides a link to not only see, but "surf" to the file (letting the browser decide how to render).
In this case:

NOTE: This example uses Java 8's stream support combined with Java NIO operations to build paths, walk the directory of files, and generate Spring HATEOAS links.

The `handleFileUpload` method is geared to handle a multi-part message: `file`. After verifying the file isn't empty, it uses Java NIO to copy the input stream to a local file.
* `GET /` looks up the current list of uploaded files from the `StorageService` and loads it into a Thymeleaf template. It calculates a link to the actual resource using `MvcUriComponentsBuilder`
* `GET /files/{filename}` loads the resource if it exists, and sends it to the browser to download using a `"Content-Disposition"` response header
* `POST /` is geared to handle a multi-part message `file` and give it to the `StorageService` for saving

NOTE: In a production scenario, you more likely would store the files in a temporary location, a database, or perhaps a NoSQL store like http://docs.mongodb.org/manual/core/gridfs[Mongo's GridFS]. It's is best to NOT load up the file system of your application with content.

== Creating a barebones template
== Creating a simple HTML template

To build something of interest, the following Thymeleaf template is a nice example of uploading files as well as showing what's been uploaded.

Expand All @@ -98,7 +90,7 @@ This template has three parts:

When configuring file uploads, it is often useful to set limits on the size of files. Imagine trying to handle a 5GB file upload! With Spring Boot, we can tune its auto-configured `MultipartConfigElement` with some property settings.

Create `src/main/resources/application.properties` and make it look like this:
Add the following properties to your existing `src/main/resources/application.properties`:

`src/main/resources/application.properties`
[source,java]
Expand Down Expand Up @@ -141,6 +133,19 @@ You should then see something like this in your browser window:
You successfully uploaded <name of your file>!
....

== Testing your application

There are multiple ways to test this particular feature in our application. Here's one example that leverages `MockMvc`, so it does not require to start the Servlet container:

`src/test/java/hello/FileUploadTests.java`
[source,java]
----
include::complete/src/test/java/hello/FileUploadTests.java[]
----

In those tests, we're using various mocks to set up the interactions with our Controller and the `StorageService` but also with the Servlet container itself by using `MockMultipartFile`.

For an example of an integration test, please check out the `FileUploadIntegrationTests` class.

== Summary

Expand Down
10 changes: 2 additions & 8 deletions complete/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,8 @@ buildscript {

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

apply plugin: 'application'
run {
args 'sample.txt'
}

jar {
baseName = 'gs-uploading-files'
version = '0.1.0'
Expand All @@ -31,8 +25,8 @@ targetCompatibility = 1.8

dependencies {
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-hateoas")
compile("org.springframework.boot:spring-boot-devtools")
testCompile("junit:junit")
compile("org.springframework.boot:spring-boot-configuration-processor")
testCompile("org.springframework.boot:spring-boot-starter-test")
}

13 changes: 10 additions & 3 deletions complete/pom.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.springframework</groupId>
Expand All @@ -21,13 +21,20 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<properties>
Expand Down
16 changes: 7 additions & 9 deletions complete/src/main/java/hello/Application.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
package hello;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

import hello.storage.StorageProperties;
import hello.storage.StorageService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.FileSystemUtils;

@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
CommandLineRunner init() {
CommandLineRunner init(StorageService storageService) {
return (args) -> {
FileSystemUtils.deleteRecursively(new File(FileUploadController.ROOT));

Files.createDirectory(Paths.get(FileUploadController.ROOT));
storageService.deleteAll();
storageService.init();
};
}
}
125 changes: 56 additions & 69 deletions complete/src/main/java/hello/FileUploadController.java
Original file line number Diff line number Diff line change
@@ -1,83 +1,70 @@
package hello;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import hello.storage.StorageFileNotFoundException;
import hello.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.io.IOException;
import java.util.stream.Collectors;

@Controller
public class FileUploadController {

private static final Logger log = LoggerFactory.getLogger(FileUploadController.class);

public static final String ROOT = "upload-dir";

private final ResourceLoader resourceLoader;

@Autowired
public FileUploadController(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}

@RequestMapping(method = RequestMethod.GET, value = "/")
public String provideUploadInfo(Model model) throws IOException {

model.addAttribute("files", Files.walk(Paths.get(ROOT))
.filter(path -> !path.equals(Paths.get(ROOT)))
.map(path -> Paths.get(ROOT).relativize(path))
.map(path -> linkTo(methodOn(FileUploadController.class).getFile(path.toString())).withRel(path.toString()))
.collect(Collectors.toList()));

return "uploadForm";
}

@RequestMapping(method = RequestMethod.GET, value = "/{filename:.+}")
@ResponseBody
public ResponseEntity<?> getFile(@PathVariable String filename) {

try {
return ResponseEntity.ok(resourceLoader.getResource("file:" + Paths.get(ROOT, filename).toString()));
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}

@RequestMapping(method = RequestMethod.POST, value = "/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {

if (!file.isEmpty()) {
try {
Files.copy(file.getInputStream(), Paths.get(ROOT, file.getOriginalFilename()));
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");
} catch (IOException|RuntimeException e) {
redirectAttributes.addFlashAttribute("message", "Failued to upload " + file.getOriginalFilename() + " => " + e.getMessage());
}
} else {
redirectAttributes.addFlashAttribute("message", "Failed to upload " + file.getOriginalFilename() + " because it was empty");
}

return "redirect:/";
}
private final StorageService storageService;

@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}

@GetMapping("/")
public String listUploadedFiles(Model model) throws IOException {

model.addAttribute("files", storageService
.loadAll()
.map(path ->
MvcUriComponentsBuilder
.fromMethodName(FileUploadController.class, "serveFile", path.getFileName().toString())
.build().toString())
.collect(Collectors.toList()));

return "uploadForm";
}

@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {

Resource file = storageService.loadAsResource(filename);
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+file.getFilename()+"\"")
.body(file);
}

@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {

storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");

return "redirect:/";
}

@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}

}
Loading

0 comments on commit f60289d

Please sign in to comment.