US02: Make it possible to upload a picture through the API

This commit is contained in:
Ivaylo Ivanov 2020-03-19 18:06:52 +01:00
parent f9a8d5016a
commit 0f5f0d2267
17 changed files with 269 additions and 31 deletions

View File

@ -12,8 +12,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@RestController
@ -50,7 +52,7 @@ public class HorseEndpoint {
return horseMapper.entityToDto(horseService.addHorse(horseEntity));
} catch (ValidationException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Error during adding new horse: " + e.getMessage(), e);
} catch (DataAccessException e) {
LOGGER.error(e.getMessage());
@ -58,4 +60,21 @@ public class HorseEndpoint {
"Something went wrong during the communication with the database", e);
}
}
@PostMapping(value = "/upload")
@ResponseStatus(HttpStatus.CREATED)
public void addImage(@RequestParam("file") MultipartFile image) {
LOGGER.info("POST " + BASE_URL + "/upload");
try {
horseService.saveImage(image);
} catch(ValidationException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Unsupported file type provided. Supported file types are jpg and png.");
} catch (IOException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
"Something went wrong while uploading the file");
}
}
}

View File

@ -14,26 +14,29 @@ public class HorseDto extends BaseDto {
private short score;
private ERace race;
private Date birthday;
private String imagePath;
private Long owner;
public HorseDto() {}
public HorseDto(String name, String description, short score, Date birthday, ERace race, Long owner) {
public HorseDto(String name, String description, short score, Date birthday, ERace race, String imagePath, Long owner) {
this.name = name;
this.description = description;
this.score = score;
this.birthday = birthday;
this.race = race;
this.imagePath = imagePath;
this.owner = owner;
}
public HorseDto(Long id, String name, String description, short score, Date birthday, ERace race, LocalDateTime created, LocalDateTime updated, Long owner) {
public HorseDto(Long id, String name, String description, short score, Date birthday, ERace race, String imagePath, LocalDateTime created, LocalDateTime updated, Long owner) {
super(id, created, updated);
this.name = name;
this.description = description;
this.score = score;
this.birthday = birthday;
this.race = race;
this.imagePath = imagePath;
this.owner = owner;
}
@ -77,6 +80,14 @@ public class HorseDto extends BaseDto {
this.race = race;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public Long getOwner() {
return owner;
}
@ -96,6 +107,7 @@ public class HorseDto extends BaseDto {
Objects.equals(score, h.score) &&
Objects.equals(birthday, h.birthday) &&
Objects.equals(race, h.race) &&
Objects.equals(imagePath, h.imagePath) &&
Objects.equals(owner, h.owner);
}
@ -113,6 +125,7 @@ public class HorseDto extends BaseDto {
", score='" + score + '\'' +
", birthday='" + df.format(birthday) + '\'' +
", race='" + race + '\'' +
", imagePath='" + imagePath + '\'' +
", owner_id='" + owner + '\'';
}

View File

@ -8,10 +8,10 @@ import org.springframework.stereotype.Component;
@Component
public class HorseMapper {
public HorseDto entityToDto(Horse horse) {
return new HorseDto(horse.getId(), horse.getName(), horse.getDescription(), horse.getScore(), horse.getBirthday(), horse.getRace(), horse.getCreatedAt(), horse.getUpdatedAt(), horse.getOwner());
return new HorseDto(horse.getId(), horse.getName(), horse.getDescription(), horse.getScore(), horse.getBirthday(), horse.getRace(), horse.getImagePath(), horse.getCreatedAt(), horse.getUpdatedAt(), horse.getOwner());
}
public Horse dtoToEntity(HorseDto horse) {
return new Horse(horse.getId(), horse.getName(), horse.getDescription(), horse.getScore(), horse.getBirthday(), horse.getRace(), horse.getOwner());
return new Horse(horse.getId(), horse.getName(), horse.getDescription(), horse.getScore(), horse.getBirthday(), horse.getRace(), horse.getImagePath(), horse.getOwner());
}
}

View File

@ -13,36 +13,40 @@ public class Horse extends BaseEntity {
private short score;
private ERace race;
private Date birthday;
private String imagePath;
private Long owner;
public Horse() {}
public Horse(String name, String description, short score, Date birthday, ERace race,Long owner) {
public Horse(String name, String description, short score, Date birthday, ERace race, String imagePath, Long owner) {
this.name = name;
this.description = description;
this.score = score;
this.birthday = birthday;
this.race = race;
this.imagePath = imagePath;
this.owner = owner;
}
public Horse(Long id, String name, String description, short score, Date birthday, ERace race, Long owner) {
public Horse(Long id, String name, String description, short score, Date birthday, ERace race, String imagePath, Long owner) {
super(id);
this.name = name;
this.description = description;
this.score = score;
this.birthday = birthday;
this.race = race;
this.imagePath = imagePath;
this.owner = owner;
}
public Horse(Long id, String name, String description, short score, Date birthday, ERace race, Long owner, LocalDateTime created, LocalDateTime updated) {
public Horse(Long id, String name, String description, short score, Date birthday, ERace race, String imagePath, Long owner, LocalDateTime created, LocalDateTime updated) {
super(id, created, updated);
this.name = name;
this.description = description;
this.score = score;
this.birthday = birthday;
this.race = race;
this.imagePath = imagePath;
this.owner = owner;
}
@ -86,6 +90,14 @@ public class Horse extends BaseEntity {
this.race = race;
}
public String getImagePath() {
return imagePath;
}
public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}
public Long getOwner() {
return owner;
}
@ -105,6 +117,7 @@ public class Horse extends BaseEntity {
Objects.equals(score, h.score) &&
Objects.equals(birthday, h.birthday) &&
Objects.equals(race, h.race) &&
Objects.equals(imagePath, h.imagePath) &&
Objects.equals(owner, h.owner);
}
@ -122,6 +135,7 @@ public class Horse extends BaseEntity {
", score='" + score + '\'' +
", birthday='" + df.format(birthday) + '\'' +
", race='" + race + '\'' +
", imagePath='" + imagePath + '\'' +
", owner='" + owner + '\'';
}

View File

@ -0,0 +1,13 @@
package at.ac.tuwien.sepm.assignment.individual.persistence;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
public interface FileDao {
/**
* Used for saving files on the local file system
* @param file file to save
*/
void save(MultipartFile file) throws IOException;
}

View File

@ -0,0 +1,52 @@
package at.ac.tuwien.sepm.assignment.individual.persistence.impl;
import at.ac.tuwien.sepm.assignment.individual.persistence.FileDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
@Repository
public class HorseFileDao implements FileDao {
@Value("${spring.upload.path}")
private String FILE_BASE_PATH;
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public HorseFileDao() {};
@Override
public void save(MultipartFile file) throws IOException {
if(file == null || file.getOriginalFilename() == null || file.isEmpty())
throw new IOException("Cannot save an empty file");
if(!file.getContentType().equals("image/jpeg") && !file.getContentType().equals("image/png"))
throw new IOException("Unsupported file type");
String fileName = file.getOriginalFilename();
LOGGER.trace("Writing file to " + FILE_BASE_PATH + fileName);
try {
// Create directory if it doesn't exist
File uploadDir = new File(FILE_BASE_PATH);
if(!uploadDir.exists())
uploadDir.mkdir();
// Save the file
if(uploadDir.exists()) {
File target = new File(FILE_BASE_PATH + fileName);
Files.write(target.toPath(), file.getBytes());
} else {
throw new FileSystemException("Base upload directory " + FILE_BASE_PATH + " does not exist");
}
} catch (Exception e) {
LOGGER.error(e.getMessage());
throw new IOException("Saving the file failed");
}
}
}

View File

@ -50,11 +50,11 @@ public class HorseJdbcDao implements HorseDao {
@Override
public Horse addHorse(Horse horse) throws DataAccessException {
LOGGER.trace("Add horse {}", horse.toString());
String sql = "INSERT INTO " + TABLE_NAME + "(name, description, score, birthday, race, owner_id, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?)";
String sql = "INSERT INTO " + TABLE_NAME + "(name, description, score, birthday, race, image_path, owner_id, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)";
try {
// Check if the constraints are violated
if(horse.getName() == null || horse.getScore() == 0 || horse.getBirthday() == null || horse.getRace() == null)
if(horse.getName() == null || horse.getScore() == 0 || horse.getBirthday() == null || horse.getRace() == null || horse.getImagePath() == null)
throw new DataIntegrityViolationException("Required parameters for horse missing");
LocalDateTime currentTime = LocalDateTime.now();
@ -74,13 +74,15 @@ public class HorseJdbcDao implements HorseDao {
ps.setDate(4, horse.getBirthday());
ps.setString(5, horse.getRace().toString()); // Convert to string to be able to save in DB
if(horse.getOwner() == null || horse.getOwner() == 0)
ps.setNull(6, Types.NULL);
else
ps.setObject(6, horse.getOwner());
ps.setString(6, horse.getImagePath());
ps.setObject(7, horse.getCreatedAt());
ps.setObject(8, horse.getUpdatedAt());
if(horse.getOwner() == null || horse.getOwner() == 0)
ps.setNull(7, Types.NULL);
else
ps.setObject(7, horse.getOwner());
ps.setObject(8, horse.getCreatedAt());
ps.setObject(9, horse.getUpdatedAt());
return ps;
}, keyHolder);
@ -98,6 +100,7 @@ public class HorseJdbcDao implements HorseDao {
}
}
private Horse mapRow(ResultSet resultSet, int i) throws SQLException {
final Horse horse = new Horse();
horse.setId(resultSet.getLong("id"));
@ -107,6 +110,7 @@ public class HorseJdbcDao implements HorseDao {
horse.setBirthday(resultSet.getDate("birthday"));
horse.setRace(ERace.valueOf(resultSet.getString("race"))); // Convert to Enum for usage in objects
horse.setOwner(resultSet.getLong("owner_id"));
horse.setImagePath(resultSet.getString("image_path"));
horse.setCreatedAt(resultSet.getTimestamp("created_at").toLocalDateTime());
horse.setUpdatedAt(resultSet.getTimestamp("updated_at").toLocalDateTime());
return horse;

View File

@ -2,6 +2,9 @@ package at.ac.tuwien.sepm.assignment.individual.service;
import at.ac.tuwien.sepm.assignment.individual.entity.Horse;
import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
public interface HorseService {
/**
@ -19,4 +22,10 @@ public interface HorseService {
* @throws NotFoundException will be thrown if the horse could not be found in the system.
*/
Horse addHorse(Horse horse);
/**
* @param img image to upload
* @throws IOException will be thrown if something goes wrong with saving the file
*/
void saveImage(MultipartFile img) throws IOException;
}

View File

@ -1,6 +1,7 @@
package at.ac.tuwien.sepm.assignment.individual.service.impl;
import at.ac.tuwien.sepm.assignment.individual.entity.Horse;
import at.ac.tuwien.sepm.assignment.individual.persistence.FileDao;
import at.ac.tuwien.sepm.assignment.individual.persistence.HorseDao;
import at.ac.tuwien.sepm.assignment.individual.service.HorseService;
import at.ac.tuwien.sepm.assignment.individual.util.ValidationException;
@ -10,32 +11,42 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.text.SimpleDateFormat;
@Service
public class SimpleHorseService implements HorseService {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final HorseDao horseDao;
private final HorseDao horseJdbcDao;
private final FileDao horseFileDao;
private final Validator validator;
@Autowired
public SimpleHorseService(HorseDao horseDao, Validator validator) {
this.horseDao = horseDao;
public SimpleHorseService(HorseDao horseJdbcDao, FileDao horseFileDao, Validator validator) {
this.horseJdbcDao = horseJdbcDao;
this.horseFileDao = horseFileDao;
this.validator = validator;
}
@Override
public Horse findOneById(Long id) {
LOGGER.trace("findOneById({})", id);
return horseDao.findOneById(id);
return horseJdbcDao.findOneById(id);
}
@Override
public Horse addHorse(Horse horse) throws ValidationException, DataAccessException {
this.validator.validateNewHorse(horse);
LOGGER.trace("addHorse({})", horse.toString());
return horseDao.addHorse(horse);
return horseJdbcDao.addHorse(horse);
}
@Override
public void saveImage(MultipartFile img) throws ValidationException, IOException {
this.validator.validateHorseImage(img);
LOGGER.trace("saveImage({})", img.getName());
horseFileDao.save(img);
}
}

View File

@ -3,6 +3,7 @@ package at.ac.tuwien.sepm.assignment.individual.util;
import at.ac.tuwien.sepm.assignment.individual.entity.Horse;
import at.ac.tuwien.sepm.assignment.individual.entity.Owner;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component
public class Validator {
@ -22,5 +23,18 @@ public class Validator {
if(horse.getScore() > 5 || horse.getScore() < 1) {
throw new ValidationException("Score value " + horse.getScore() + " not allowed. The score must be an integer between 1 and 5");
}
if(!horse.getImagePath().endsWith(".png") && !horse.getImagePath().endsWith(".jpg") && !horse.getImagePath().endsWith(".jpeg")) {
throw new ValidationException("Unsupported file type supplied. Supported file types are jpg and png");
}
}
public void validateHorseImage(MultipartFile image) throws ValidationException {
if(image == null || image.getContentType() == null || image.isEmpty()) {
throw new ValidationException("No image supplied");
}
if(!image.getContentType().equals("image/png") && !image.getContentType().equals("image/jpeg")) {
throw new ValidationException("Unsupported file type supplied: " + image.getContentType());
}
}
}

View File

@ -15,5 +15,12 @@ spring:
h2:
console:
enabled: true
upload:
path: /tmp/uploads/
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 15MB
server:
port: 8080

View File

@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS horse
score TINYINT NOT NULL CHECK(score >= 1 AND score <= 5),
birthday DATE NOT NULL,
race RACE NOT NULL,
image_path VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);

View File

@ -6,20 +6,25 @@ import at.ac.tuwien.sepm.assignment.individual.endpoint.dto.HorseDto;
import at.ac.tuwien.sepm.assignment.individual.enums.ERace;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.event.annotation.AfterTestMethod;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.sql.Date;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class HorseEndpointTest {
@Value("${spring.upload.path}")
private String FILE_BASE_PATH;
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
private static final String BASE_URL = "http://localhost:";
private static final String HORSE_URL = "/horses";
@ -31,7 +36,7 @@ public class HorseEndpointTest {
@DisplayName("Adding a new horse with the correct parameters will return HTTP 201 and the new HorseDto")
public void addingNewHorse_correctParameters_shouldReturnStatus201AndHorse() {
String birthday = "2020-01-01";
HorseDto newHorse = new HorseDto("Zephyr", "Nice horse", (short) 4, Date.valueOf(birthday), ERace.APPALOOSA, null);
HorseDto newHorse = new HorseDto("Zephyr", "Nice horse", (short) 4, Date.valueOf(birthday), ERace.APPALOOSA, "files/test.png", null);
HttpEntity<HorseDto> request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> response = REST_TEMPLATE
@ -43,6 +48,32 @@ public class HorseEndpointTest {
assertEquals(newHorse.getDescription(), response.getBody().getDescription());
assertEquals(newHorse.getScore(), response.getBody().getScore());
assertEquals(newHorse.getBirthday().toString(), response.getBody().getBirthday().toString());
assertEquals(newHorse.getImagePath(), response.getBody().getImagePath());
assertEquals(newHorse.getOwner(), response.getBody().getOwner());
}
@Test
@DisplayName("Uploading an image in the correct format will return HTTP 201")
public void addingNewImage_correctFormat_shouldReturnStatus201() {
try {
// More information:
// https://www.baeldung.com/spring-rest-template-multipart-upload
// https://github.com/spring-guides/gs-uploading-files/blob/master/complete/src/test/java/com/example/uploadingfiles/FileUploadIntegrationTests.java
// Get the file
ClassPathResource image = new ClassPathResource("horse.jpg", getClass());
// Set the body
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", image);
// Create the HTTP Entity
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body);
ResponseEntity<String> response = REST_TEMPLATE.postForEntity(BASE_URL + port + HORSE_URL + "/upload", request, String.class);
assertEquals(HttpStatus.CREATED, response.getStatusCode());
} finally {
new File(FILE_BASE_PATH + "horse.jpg").delete();
}
}
}

View File

@ -21,7 +21,7 @@ public abstract class HorseDaoTestBase {
@DisplayName("Adding a new horse with the correct parameters should return the horse")
public void addingNewHorse_correctParameters_shouldReturnHorse() {
String birthday = "2020-01-01";
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf(birthday), ERace.APPALOOSA, null);
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf(birthday), ERace.APPALOOSA, "files/test.png", null);
Horse savedHorse = horseDao.addHorse(newHorse);
assertEquals(newHorse, savedHorse);
}
@ -30,7 +30,7 @@ public abstract class HorseDaoTestBase {
@DisplayName("Adding a new horse with the incorrect parameters should throw DataAccessException")
public void addingNewHorse_incorrectParameters_shouldThrowDataAccess() {
String birthday = "2020-01-01";
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 80, Date.valueOf(birthday), null, null);
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 80, Date.valueOf(birthday), null, null, null);
assertThrows(DataAccessException.class, () -> horseDao.addHorse(newHorse));
}

View File

@ -0,0 +1,50 @@
package at.ac.tuwien.sepm.assignment.individual.unit.persistence;
import static org.junit.jupiter.api.Assertions.*;
import at.ac.tuwien.sepm.assignment.individual.persistence.impl.HorseFileDao;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@SpringBootTest
@ActiveProfiles("test")
public class HorseFileDaoTest {
@Value("${spring.upload.path}")
private String FILE_BASE_PATH;
@Autowired
HorseFileDao horseFileDao;
@Test
@DisplayName("Creating a file with the correct type should result in a saved file")
public void savingFile_correctType_shouldWriteFile() throws FileNotFoundException, IOException {
File image = new File("src/test/resources/at/ac/tuwien/sepm/assignment/individual/integration/horse.jpg");
horseFileDao.save(new MockMultipartFile("file", image.getName(), MediaType.IMAGE_JPEG_VALUE, new FileInputStream(image)));
File savedImage = new File(FILE_BASE_PATH + "horse.jpg");
assertTrue(savedImage.exists());
assertTrue(savedImage.isFile());
}
@Test
@DisplayName("Creating a file with the correct type should result in a saved file")
public void savingFile_incorrectType_shouldThrowIOException() throws FileNotFoundException, IOException {
File image = new File("src/test/resources/at/ac/tuwien/sepm/assignment/individual/integration/horse.jpg");
assertThrows(IOException.class, () -> horseFileDao.save(new MockMultipartFile("file", image.getName(), MediaType.TEXT_HTML_VALUE, new FileInputStream(image))));
}
@AfterEach
public void cleanup() {
new File(FILE_BASE_PATH + "horse.jpg").delete();
}
}

View File

@ -29,7 +29,7 @@ public class HorseServiceTest {
@DisplayName("Adding a new horse with the correct parameters will return the new horse")
public void addingNewHorse_correctParameters_shouldReturnHorse() {
String birthday = "2020-01-01";
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf(birthday), ERace.APPALOOSA, null);
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf(birthday), ERace.APPALOOSA, "files/test.png", null);
Horse savedHorse = horseService.addHorse(newHorse);
assertEquals(newHorse, savedHorse);
}
@ -38,7 +38,7 @@ public class HorseServiceTest {
@DisplayName("Adding a new horse with the incorrect parameters will throw a ValidationException")
public void addingNewHorse_incorrectParameters_shouldThrowValidation() {
String birthday = "2020-01-01";
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 80, Date.valueOf(birthday), null, null);
Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 80, Date.valueOf(birthday), null, null, null);
assertThrows(ValidationException.class, () -> horseService.addHorse(newHorse));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB