From 5c53ad4f010a7b271420ae83f1dd72b45a7a8ed1 Mon Sep 17 00:00:00 2001 From: Ivaylo Ivanov Date: Tue, 24 Mar 2020 16:38:45 +0100 Subject: [PATCH] US08: Add an owner delete endpoint to the API --- .../individual/endpoint/OwnerEndpoint.java | 23 ++++++ .../individual/persistence/OwnerDao.java | 7 ++ .../persistence/impl/HorseJdbcDao.java | 2 + .../persistence/impl/OwnerJdbcDao.java | 53 +++++++++++++- .../individual/service/OwnerService.java | 9 ++- .../service/impl/SimpleOwnerService.java | 8 +++ .../assignment/individual/util/Validator.java | 3 - .../integration/OwnerEndpointTest.java | 71 ++++++++++++++++++- .../unit/persistence/OwnerDaoTestBase.java | 42 +++++++++++ 9 files changed, 210 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/endpoint/OwnerEndpoint.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/endpoint/OwnerEndpoint.java index 54db7ba..c238001 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/endpoint/OwnerEndpoint.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/endpoint/OwnerEndpoint.java @@ -5,6 +5,7 @@ import at.ac.tuwien.sepm.assignment.individual.endpoint.mapper.OwnerMapper; import at.ac.tuwien.sepm.assignment.individual.entity.Owner; import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException; import at.ac.tuwien.sepm.assignment.individual.service.OwnerService; + import java.lang.invoke.MethodHandles; import at.ac.tuwien.sepm.assignment.individual.util.ValidationException; @@ -12,6 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -81,4 +83,25 @@ public class OwnerEndpoint { "The requested owner could not be found"); } } + + @DeleteMapping(value = "/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteOwner(@PathVariable("id") Long id) { + LOGGER.info("DELETE " + BASE_URL + "/{}", id); + try { + ownerService.deleteOwner(id); + } catch (DataIntegrityViolationException e) { + LOGGER.error(e.getMessage()); + throw new ResponseStatusException(HttpStatus.FORBIDDEN, + "The requested owner cannot be deleted because there are horses that are assigned to him/her"); + }catch (DataAccessException e) { + LOGGER.error(e.getMessage()); + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, + "Something went wrong during the communication with the database"); + } catch (NotFoundException e) { + LOGGER.error(e.getMessage()); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, + "The requested owner has not been found"); + } + } } diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/OwnerDao.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/OwnerDao.java index f2aefb5..c51d87e 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/OwnerDao.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/OwnerDao.java @@ -29,4 +29,11 @@ public interface OwnerDao { * @throws DataAccessException will be thrown if something goes wrong during the database access. */ Owner updateOwner(Owner owner) throws DataAccessException; + + /** + * @param id of the owner to delete + * @throws DataAccessException will be thrown if something goes wrong during the database access. + * @throws NotFoundException will be thrown if the owner could not be found in the database. + */ + void deleteOwner(Long id) throws DataAccessException, NotFoundException; } diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/HorseJdbcDao.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/HorseJdbcDao.java index a321702..616fdfd 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/HorseJdbcDao.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/HorseJdbcDao.java @@ -7,6 +7,7 @@ import at.ac.tuwien.sepm.assignment.individual.persistence.FileDao; import at.ac.tuwien.sepm.assignment.individual.persistence.HorseDao; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.jdbc.core.JdbcTemplate; @@ -35,6 +36,7 @@ public class HorseJdbcDao implements HorseDao { private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final FileDao fileDao = new HorseFileDao(); + @Autowired public HorseJdbcDao(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.jdbcTemplate = jdbcTemplate; this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/OwnerJdbcDao.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/OwnerJdbcDao.java index edeb9e7..e589cc9 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/OwnerJdbcDao.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/persistence/impl/OwnerJdbcDao.java @@ -1,20 +1,27 @@ package at.ac.tuwien.sepm.assignment.individual.persistence.impl; +import at.ac.tuwien.sepm.assignment.individual.entity.Horse; import at.ac.tuwien.sepm.assignment.individual.entity.Owner; import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException; import at.ac.tuwien.sepm.assignment.individual.persistence.OwnerDao; + import java.lang.invoke.MethodHandles; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; @@ -25,6 +32,7 @@ import org.springframework.stereotype.Repository; public class OwnerJdbcDao implements OwnerDao { private static final String TABLE_NAME = "Owner"; + private static final String HORSE_TABLE_NAME = "Horse"; private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final JdbcTemplate jdbcTemplate; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; @@ -92,7 +100,7 @@ public class OwnerJdbcDao implements OwnerDao { try { if(owner.getId() == null || owner.getId() == 0) - throw new DataIntegrityViolationException("Horse Id missing or 0"); + throw new DataIntegrityViolationException("Owner Id missing or 0"); this.validateOwner(owner); @@ -123,11 +131,52 @@ public class OwnerJdbcDao implements OwnerDao { } } + @Override + public void deleteOwner(Long id) throws DataAccessException, NotFoundException { + Owner ownerToDelete = this.findOneById(id); + LOGGER.trace("Delete owner with id {}", id); + final String sql = "DELETE FROM " + TABLE_NAME + " WHERE id=?"; + + if (ownerOwnsHorses(id)) + throw new DataIntegrityViolationException("Deleting owner failed, owner has horses assigned"); + + try { + int changes = jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql); + ps.setLong(1, id); + return ps; + }); + + if (changes == 0) + throw new DataAccessException("Deleting owner failed, no rows affected") {}; + + } catch(DataAccessException e){ + // We are doing this in order to not change the exception type + throw new DataAccessException("Deleting records failed", e) {}; + } + } + private void validateOwner(Owner owner) throws DataIntegrityViolationException { if(owner.getName() == null || owner.getName().isEmpty()) throw new DataIntegrityViolationException("Required parameters for owner missing"); } + private boolean ownerOwnsHorses(Long ownerId) { + final String sql = "SELECT * FROM " + HORSE_TABLE_NAME + " WHERE owner_id=?"; + + try { + jdbcTemplate.queryForObject(sql, new Object[] {ownerId}, BeanPropertyRowMapper.newInstance(Horse.class)); + } catch(EmptyResultDataAccessException e) { + // If empty, return false + return false; + } catch (IncorrectResultSizeDataAccessException e) { + // If incorrect size above 0, return true + return true; + } + + return true; + } + private Owner mapRow(ResultSet resultSet, int i) throws SQLException { final Owner owner = new Owner(); owner.setId(resultSet.getLong("id")); diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/OwnerService.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/OwnerService.java index a053393..21fcf1c 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/OwnerService.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/OwnerService.java @@ -5,8 +5,6 @@ import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException; import at.ac.tuwien.sepm.assignment.individual.util.ValidationException; import org.springframework.dao.DataAccessException; -import java.io.IOException; - public interface OwnerService { @@ -33,4 +31,11 @@ public interface OwnerService { * @throws DataAccessException will be thrown if the owner could not be saved in the database. */ Owner updateOwner(Owner owner) throws ValidationException, DataAccessException; + + /** + * @param id of the owner to delete + * @throws NotFoundException will be thrown if the owner could not be found in the system + * @throws DataAccessException will be thrown if the owner could not be deleted from the database + */ + void deleteOwner(Long id) throws NotFoundException, DataAccessException; } diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/impl/SimpleOwnerService.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/impl/SimpleOwnerService.java index b38b970..d544fba 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/impl/SimpleOwnerService.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/service/impl/SimpleOwnerService.java @@ -1,10 +1,12 @@ package at.ac.tuwien.sepm.assignment.individual.service.impl; import at.ac.tuwien.sepm.assignment.individual.entity.Owner; +import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException; import at.ac.tuwien.sepm.assignment.individual.persistence.OwnerDao; import at.ac.tuwien.sepm.assignment.individual.service.OwnerService; import at.ac.tuwien.sepm.assignment.individual.util.ValidationException; import at.ac.tuwien.sepm.assignment.individual.util.Validator; + import java.lang.invoke.MethodHandles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,4 +46,10 @@ public class SimpleOwnerService implements OwnerService { this.validator.validateUpdateOwner(owner); return ownerDao.updateOwner(owner); } + + @Override + public void deleteOwner(Long id) throws NotFoundException, DataAccessException { + LOGGER.trace("deleteOwner({})", id); + ownerDao.deleteOwner(id); + } } diff --git a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/util/Validator.java b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/util/Validator.java index 20c5c8a..2870f85 100644 --- a/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/util/Validator.java +++ b/backend/src/main/java/at/ac/tuwien/sepm/assignment/individual/util/Validator.java @@ -11,9 +11,6 @@ import java.util.Map; @Component public class Validator { - - - public void validateNewOwner(Owner owner) throws ValidationException { if(owner.getName() == null || owner.getName().isEmpty()) { throw new ValidationException("Required value name missing"); diff --git a/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/integration/OwnerEndpointTest.java b/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/integration/OwnerEndpointTest.java index 8bc5830..1e21484 100644 --- a/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/integration/OwnerEndpointTest.java +++ b/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/integration/OwnerEndpointTest.java @@ -1,17 +1,24 @@ package at.ac.tuwien.sepm.assignment.individual.integration; +import static at.ac.tuwien.sepm.assignment.individual.base.TestData.HORSE_URL; import static org.junit.jupiter.api.Assertions.*; +import at.ac.tuwien.sepm.assignment.individual.endpoint.dto.HorseDto; import at.ac.tuwien.sepm.assignment.individual.endpoint.dto.OwnerDto; +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.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import java.sql.Date; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") public class OwnerEndpointTest { @@ -62,5 +69,67 @@ public class OwnerEndpointTest { assertEquals(newOwner.getId(), response.getBody().getId()); assertEquals(newOwner.getName(), response.getBody().getName()); } -} + @Test + @DisplayName("Deleting an existing owner without owners will return HTTP 204") + public void deletingOwner_existingNoOwnersOwned_shouldReturnStatus204() { + // Create the owner + OwnerDto newOwner = new OwnerDto("Chad"); + + HttpEntity request = new HttpEntity<>(newOwner); + ResponseEntity response = REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL, HttpMethod.POST, request, OwnerDto.class); + + // Delete and test if deleted + ResponseEntity res = REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL + '/' + response.getBody().getId(), HttpMethod.DELETE, null, new ParameterizedTypeReference() {}); + + assertEquals(res.getStatusCode(), HttpStatus.NO_CONTENT); + } + + @Test + @DisplayName("Deleting an existing owner without horses will return HTTP 204") + public void deletingOwner_existingNoHorsesOwned_shouldReturnStatus204() { + // Create the owner + OwnerDto newOwner = new OwnerDto("Chad"); + + HttpEntity request = new HttpEntity<>(newOwner); + ResponseEntity response = REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL, HttpMethod.POST, request, OwnerDto.class); + + // Delete and test if deleted + ResponseEntity res = REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL + '/' + response.getBody().getId(), HttpMethod.DELETE, null, new ParameterizedTypeReference() {}); + + assertEquals(res.getStatusCode(), HttpStatus.NO_CONTENT); + } + + @Test + @DisplayName("Deleting an nonexistent owner will return HTTP 404") + public void deletingOwner_nonexistent_shouldReturnStatus404() { + assertThrows(HttpClientErrorException.NotFound.class, () -> + REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL + "/0", HttpMethod.DELETE, null, new ParameterizedTypeReference() {})); + } + + @Test + @DisplayName("Deleting an exiting owner with horses will return HTTP 403") + public void deletingOwner_existingHorsesOwned_shouldReturnStatus403() { + // Create the owner + OwnerDto newOwner = new OwnerDto("Chad"); + + HttpEntity request = new HttpEntity<>(newOwner); + OwnerDto savedOwner = REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL, HttpMethod.POST, request, OwnerDto.class).getBody(); + + // Create the horse + HorseDto newHorse = new HorseDto("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", savedOwner.getId()); + + request = new HttpEntity(newHorse); + REST_TEMPLATE.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class); + + assertThrows(HttpClientErrorException.Forbidden.class, () -> + REST_TEMPLATE + .exchange(BASE_URL + port + OWNER_URL + "/" + savedOwner.getId(), HttpMethod.DELETE, null, new ParameterizedTypeReference() {})); + } +} diff --git a/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/unit/persistence/OwnerDaoTestBase.java b/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/unit/persistence/OwnerDaoTestBase.java index 8e51be4..5e0e14d 100644 --- a/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/unit/persistence/OwnerDaoTestBase.java +++ b/backend/src/test/java/at/ac/tuwien/sepm/assignment/individual/unit/persistence/OwnerDaoTestBase.java @@ -2,13 +2,17 @@ package at.ac.tuwien.sepm.assignment.individual.unit.persistence; import static org.junit.jupiter.api.Assertions.*; +import at.ac.tuwien.sepm.assignment.individual.entity.Horse; import at.ac.tuwien.sepm.assignment.individual.entity.Owner; +import at.ac.tuwien.sepm.assignment.individual.enums.ERace; import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException; +import at.ac.tuwien.sepm.assignment.individual.persistence.HorseDao; import at.ac.tuwien.sepm.assignment.individual.persistence.OwnerDao; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; import java.io.IOException; import java.sql.Date; @@ -18,6 +22,9 @@ public abstract class OwnerDaoTestBase { @Autowired OwnerDao ownerDao; + @Autowired + HorseDao horseDao; + @Test @DisplayName("Finding owner by non-existing ID should throw NotFoundException") public void findingOwnerById_nonExisting_shouldThrowNotFoundException() { @@ -70,4 +77,39 @@ public abstract class OwnerDaoTestBase { newOwner.setName(""); assertThrows(DataAccessException.class, () -> ownerDao.updateOwner(newOwner)); } + + @Test + @DisplayName("Deleting an existing owner without horses should delete the owner") + public void deletingOwner_existingOwnerNoHorsesOwned_shouldDeleteOwner() throws IOException { + // Create the owner + Owner newOwner = new Owner("Chad"); + Owner savedOwner = ownerDao.addOwner(newOwner); + + // Delete the owner + ownerDao.deleteOwner(savedOwner.getId()); + + // Check if deleted + assertThrows(NotFoundException.class, () -> ownerDao.findOneById(savedOwner.getId())); + } + + @Test + @DisplayName("Deleting an nonexistent owner should throw NotFoundException") + public void deletingOwner_nonexistent_shouldThrowNotFound() throws IOException { + assertThrows(NotFoundException.class, () -> ownerDao.deleteOwner(null)); + } + + @Test + @DisplayName("Deleting an existing owner with horses should throw DataIntegrityViolationException") + public void deletingHorse_existing_shouldDeleteHorse() { + // Create the owner + Owner newOwner = new Owner("Chad"); + Owner savedOwner = ownerDao.addOwner(newOwner); + + // Create the horse + Horse newHorse = new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", savedOwner.getId()); + Horse savedHorse = horseDao.addHorse(newHorse); + + // Delete the owner + assertThrows(DataIntegrityViolationException.class, () -> ownerDao.deleteOwner(savedOwner.getId())); + } }