What You'll Learn

Many web applications enable the user to upload a file or document.

In our application, we've prepared the ground for the charities to have a logo, but we've never provided the means to upload it.

This tutorial is illustrative. There are plenty of other ways of dealing with files other than that shown here.

In this tutorial, we've made the decision to store the images in the database using LOBs (Large OBjects). We could have used the file system or a separate database (such as Mongo GridFS).

Adding upload support to a HTML form is pretty standard.

In charity-form.html

    <form th:action="@{/admin/charity}" enctype="multipart/form-data" th:method="post" th:object="${charityForm}">


        <div class="mb-3">
            <label for="logoFile">Charity Purpose: </label>
            <input th:field="*{logoFile}" type="file"/>
        </div>



    </form>

There are two changes. Firsly add the enctype attribute to the form element. Secondly, add a new input field with type="file". We can still use a Thymeleaf attribute to refer to the field.

In CharityForm.java, add...

    private MultipartFile logoFile;

    public CharityForm(CharityDTO charityDTO) {
        this(charityDTO.getId(),
             charityDTO.getName(),
             charityDTO.getRegistration(),
             charityDTO.getAcronym(),
             charityDTO.getMissionStatement(),
             charityDTO.getLogoFileName(),
             null);
    }

We add a field of type MultipartFile and we update the constructor that takes a DTO.

By now, the mapping from the form to the form object should be apparent.

The controller method needs one extra line and one change.

In CharityAdminController.java - charityFormPost()

    if (bindingResult.hasErrors()) {
        log.debug("THERE ARE ERRORS" + bindingResult.getAllErrors());
        return "admin/charity-form";
        }

    //THIS IS THE NEW LINE
    String fileId = imageServer.saveImage(charityForm.getLogoFile());

    //NOTE THE ONE CHANGE IN THE DTO CONSTRUCTOR CALL
            CharityDTO charityDTO = new CharityDTO(charityForm.getId(),
            charityForm.getName(),
            charityForm.getRegistration(),
            charityForm.getAcronym(),
            fileId,  //THIS HAS CHANGED.
            charityForm.getMissionStatement(),
            Boolean.FALSE);

The controller uses an imageServer (you need to create this field and inject it). We'll create the image server in a minute and then you should be able to do the injection yourself.

The image server saves the image and returns an ID. That id is then stored in the charity DTO as the logoFileName.

The image server will store images in the database. Let's look at how we can use JPA to do that.

We can use a normal JPA entity.

Create DBFile.java

(see the repository for the package structure)

@Entity
@Table(name = "files")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DBFile {
  @Id
  @GeneratedValue(generator = "uuid")
  @GenericGenerator(name = "uuid", strategy = "uuid2")
  private String id;
  private String fileName;
  private String fileType;
  @Lob
  private byte[] data;
  
  public DBFile(String file, String type, byte[] data) {
    this(null, file, type, data);
  }
}

There are a couple of things to notice. The id field uses a UUID generator. This will generate the id in Java rather than use a DB sequence.

We then have a series of fields, one of which is annotated with @Lob. This is part of the JPA specification and tells JPA that this data should be mapped to a database LOB column.

Essentially, we are creating an entity that maps an id to a large object.

Create the repository

We create an interface and then create a repository that implements that interface.

Create DBFileStore

public interface DBFileStore {
  DBFile save(DBFile file);
  Optional<DBFile> findById(String id);
}

and then create DBFIleStoreRepo

public interface DBFileStoreRepo extends JpaRepository<DBFile, String>, DBFileStore {
}

This is a standard JPA repository and will save and find a DBFile entity.

The image server component is only responsible for the saving of the image. Again, we create an interface and an implementation.

Create ImageServer.java

public interface ImageServer {
    String saveImage(MultipartFile upload);
}

Create ImageServerDBImpl.java

@Service
public class ImageServerDBImpl implements ImageServer{

    private DBFileStore dbFileStore;

    public ImageServerDBImpl(DBFileStore aDbFileStore){
        dbFileStore = aDbFileStore;
    }

    @Override
    public String saveImage(MultipartFile upload) {

        String fileName = StringUtils.cleanPath(upload.getOriginalFilename());
        String fileId = "";

        try {
            DBFile dbFile = new DBFile(fileName, upload.getContentType(), upload.getBytes());

            dbFile = dbFileStore.save(dbFile);
            fileId = dbFile.getId();

        } catch (IOException e) {
            e.printStackTrace();//TODO replace with own exception
        }
        return fileId;
    }
}

The class is a @Service bean so it can be injected into the controller.

It depends on a DBFileStore. The code constructs a DBFile entity from the MultipartFile and saves it. It gets back the allocated id and returns that. This allows the logoFileName to be set to the UUID.

Now we need a way of getting those images out so that they can be included in a web page. For this, we implement a controller.

Create FileController.java

@RestController
public class FileController {

  @Value("classpath:static/images/default-logo.jpg")
  Resource defaultLogo;

  private DBFileStore dbFileStore;

  @Autowired
  public FileController(DBFileStore aDbFileStore) {
    dbFileStore = aDbFileStore;
  }

  @GetMapping("logo/{fileId}")
  public ResponseEntity<Resource> downloadFile(@PathVariable String fileId) {
    // Load file from database
    Optional<DBFile> dbFile = dbFileStore.findById(fileId);

    if (dbFile.isPresent()) {
      DBFile theFile = dbFile.get();

      return ResponseEntity.ok()
              .contentType(MediaType.parseMediaType(theFile.getFileType()))
              .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + theFile.getFileName() + "\"")
              .body(new ByteArrayResource(theFile.getData()));
    } else {
      return ResponseEntity.ok().body(defaultLogo);
    }
  }
}

The controller handles GET requests to ‘/logo/{the file id}'. The file id is passed back in the DTO. The controller uses the DBFileStore to get the image data.

Once it has the data, it uses a ResponseEntity object to return the file with the type, the file name and the data. If the URL can't be found in the database, the controller plays the standard image. (It would be better to pass the alternative image path as a parameter).

We can now use the image URL in our templates.

In charity-home.html

    <div class="col-sm">
        <img width="200px" alt="Charity Logo" th:attr="src=@{'/logo/'+${charity.logoFileName}}"/>
    </div>

This is an illustrative tutorial showing one-way of handling file uploads.

It would be better to separate the design so that there is a component that takes the file type, name, data and an identifier and stores the image to a service's specification. That component can then be replaced easily by different backing engines (DB, files, edge servers, etc).

Equally, that service would be able to return the relevant data to any client meaning that a controller could simply transform a URL into a service call and then map the data back into a response.