US05: Make it possible for the user to search for horses using the API

This commit is contained in:
Ivaylo Ivanov 2020-03-22 12:48:59 +01:00
parent a3b1185494
commit 129b3c534b
12 changed files with 391 additions and 15 deletions

View File

@ -17,6 +17,8 @@ import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping(at.ac.tuwien.sepm.assignment.individual.endpoint.HorseEndpoint.BASE_URL)
@ -39,7 +41,27 @@ public class HorseEndpoint {
try {
return horseMapper.entityToDto(horseService.findOneById(id));
} catch (NotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Error during reading horse", e);
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Error during reading horse");
}
}
@GetMapping
@ResponseStatus(HttpStatus.OK)
public List<HorseDto> getAll(@RequestParam Map<String, String> filters) {
try {
if(filters.isEmpty()) {
LOGGER.info("GET " + BASE_URL);
return horseMapper.entityListToDtoList(horseService.getAll());
} else {
LOGGER.info("GET " + BASE_URL + "/ with filters" + filters.entrySet());
return horseMapper.entityListToDtoList(horseService.getFiltered(filters));
}
} catch (NotFoundException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No horses found");
} catch (ValidationException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The request contains filters with bad values: " + e.getMessage());
}
}
@ -53,7 +75,7 @@ public class HorseEndpoint {
} catch (ValidationException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Error during adding new horse: " + e.getMessage(), e);
"Error during adding new horse: " + e.getMessage());
} catch (DataAccessException e) {
LOGGER.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,

View File

@ -2,9 +2,11 @@ package at.ac.tuwien.sepm.assignment.individual.endpoint.mapper;
import at.ac.tuwien.sepm.assignment.individual.endpoint.dto.HorseDto;
import at.ac.tuwien.sepm.assignment.individual.entity.Horse;
import at.ac.tuwien.sepm.assignment.individual.entity.Owner;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class HorseMapper {
public HorseDto entityToDto(Horse horse) {
@ -14,4 +16,13 @@ public class HorseMapper {
public Horse dtoToEntity(HorseDto horse) {
return new Horse(horse.getId(), horse.getName(), horse.getDescription(), horse.getScore(), horse.getBirthday(), horse.getRace(), horse.getImagePath(), horse.getOwner());
}
public List<HorseDto> entityListToDtoList(List<Horse> horseEntities) {
List<HorseDto> horseDtos = new ArrayList<>();
for(Horse horse: horseEntities) {
horseDtos.add(entityToDto(horse));
}
return horseDtos;
}
}

View File

@ -4,5 +4,24 @@ public enum ERace {
ARABIAN,
MORGAN,
PAINT,
APPALOOSA
APPALOOSA;
public static boolean contains(String val) {
// Loop through all values and check if the string is one of them
// https://stackoverflow.com/a/9275694
for(ERace race:values())
if (race.name().equals(val))
return true;
return false;
}
public static String valuesToString() {
String res = "";
for(ERace race: values()) {
res += race + ", ";
}
// Return the result without the last comma and space
return res.substring(0, res.length() - 2);
}
}

View File

@ -31,7 +31,7 @@ public class DataGeneratorBean {
@PostConstruct
void insertDummyData() {
try {
ScriptUtils.executeSqlScript(source.getConnection(), new ClassPathResource("sql/insertData.sql"));
ScriptUtils.executeSqlScript(source.getConnection(), new ClassPathResource("src/test/resources/sql/insertData.sql"));
} catch (Exception e) {
LOGGER.error("Error inserting test data", e);
}

View File

@ -5,6 +5,8 @@ import at.ac.tuwien.sepm.assignment.individual.exception.NotFoundException;
import org.springframework.dao.DataAccessException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public interface HorseDao {
@ -16,6 +18,21 @@ public interface HorseDao {
*/
Horse findOneById(Long id);
/**
* @return a list of all horses in the system
* @throws NotFoundException will be thrown if no horses are present in the database
* @throws DataAccessException will be thrown if something goes wrong during the database access
*/
List<Horse> getAll() throws NotFoundException;
/**
* @param filters to use for filtering the horses
* @return a list of all horses that fill the criteria
* @throws NotFoundException wil be thrown if no horses fill the criteria
* @throws DataAccessException will be thrown if something goes wrong during the database access
*/
List<Horse> getFiltered(Map<String, String> filters) throws NotFoundException;
/**
* @param horse that specifies the horse to add
* @return the newly created horse

View File

@ -5,27 +5,27 @@ 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.FileDao;
import at.ac.tuwien.sepm.assignment.individual.persistence.HorseDao;
import at.ac.tuwien.sepm.assignment.individual.util.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import javax.xml.crypto.Data;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.sql.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class HorseJdbcDao implements HorseDao {
@ -52,6 +52,62 @@ public class HorseJdbcDao implements HorseDao {
}
@Override
public List<Horse> getAll() throws NotFoundException {
LOGGER.trace("Get all horses");
final String sql = "SELECT * FROM " + TABLE_NAME;
List<Horse> horses = jdbcTemplate.query(sql, new Object[] { }, this::mapRow);
if(horses.isEmpty()) throw new NotFoundException("No horses found in the database");
return horses;
}
@Override
public List<Horse> getFiltered(Map<String, String> filters) throws NotFoundException {
LOGGER.trace("Get all horses with filters " + filters.entrySet());
final String sql = "SELECT * FROM " + TABLE_NAME + " WHERE UPPER(name) LIKE :name AND UPPER(description) LIKE :description AND race LIKE :race AND score LIKE :score AND birthday <= :birthday";
// Create a list to hold the results
List<Horse> horses = new ArrayList<>();
// Create a map to hold the sql filters with all values set as wildcards
Map<String, String> queryFilters = new HashMap<>();
queryFilters.put("name", "%_%");
queryFilters.put("description", "%_%");
queryFilters.put("race", "%_%");
queryFilters.put("score", "%");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
queryFilters.put("birthday", df.format(new java.sql.Date(System.currentTimeMillis())));
// Go through the supplied filters and find set values
if(filters.get("name") != null)
queryFilters.replace("name", '%' + filters.get("name").toUpperCase() + '%');
if(filters.get("description") != null)
queryFilters.replace("description", '%' + filters.get("description").toUpperCase() + '%');
if(filters.get("race") != null)
queryFilters.replace("race", filters.get("race").toUpperCase());
if(filters.get("score") != null)
queryFilters.replace("score", filters.get("score"));
if(filters.get("birthday") != null)
queryFilters.replace("birthday", filters.get("birthday"));
// Create an map sql parameter source for use in the query
MapSqlParameterSource sqlMap = new MapSqlParameterSource();
sqlMap.addValues(queryFilters);
horses = namedParameterJdbcTemplate.query(sql, sqlMap, this::mapRow);
if(horses.isEmpty()) throw new NotFoundException("No horses found in the database");
return horses;
}
@Override
public Horse addHorse(Horse horse) throws DataAccessException {
LOGGER.trace("Add horse {}", horse.toString());
@ -98,8 +154,7 @@ public class HorseJdbcDao implements HorseDao {
} catch (DataAccessException e) {
// We are doing this in order to not change the exception type
throw new DataAccessException("Adding new records failed", e) {
};
throw new DataAccessException("Adding new records failed", e) {};
}
}

View File

@ -7,6 +7,8 @@ import org.springframework.dao.DataAccessException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public interface HorseService {
/**
@ -17,6 +19,20 @@ public interface HorseService {
*/
Horse findOneById(Long id);
/**
* @return a list of all horses in the database
* @throws NotFoundException will be thrown if there are no horses in the database
*/
List<Horse> getAll() throws NotFoundException;
/**
* @param filters to use for filtering the horses
* @return a list of all horses that fill the criteria
* @throws NotFoundException will be thrown if no horses fill the criteria
* @throws ValidationException will be thrown if the filter contains bad values
*/
List<Horse> getFiltered(Map<String, String> filters) throws NotFoundException, ValidationException;
/**
* @param horse to add.
* @return the new horse.

View File

@ -16,6 +16,8 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
@Service
public class SimpleHorseService implements HorseService {
@ -37,6 +39,19 @@ public class SimpleHorseService implements HorseService {
return horseJdbcDao.findOneById(id);
}
@Override
public List<Horse> getAll() throws NotFoundException {
LOGGER.trace("getAll()");
return horseJdbcDao.getAll();
}
@Override
public List<Horse> getFiltered(Map<String, String> filters) throws NotFoundException, ValidationException {
LOGGER.trace("getFiltered({})", filters.entrySet());
this.validator.validateHorseFilter(filters);
return horseJdbcDao.getFiltered(filters);
}
@Override
public Horse addHorse(Horse horse) throws ValidationException, DataAccessException {
this.validator.validateNewHorse(horse);

View File

@ -2,9 +2,13 @@ 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 at.ac.tuwien.sepm.assignment.individual.enums.ERace;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.sql.Date;
import java.util.Map;
@Component
public class Validator {
@ -52,4 +56,26 @@ public class Validator {
}
}
public void validateHorseFilter(Map<String, String> filters) throws ValidationException {
if(filters.get("score") != null) {
try {
if (Integer.parseInt(filters.get("score")) < 1 || Integer.parseInt(filters.get("score")) > 5)
throw new ValidationException("Score value " + filters.get("score") + " not allowed. The score must be an integer between 1 and 5");
} catch(NumberFormatException e) {
throw new ValidationException("Score value " + filters.get("score") + " not allowed. The score must be an integer between 1 and 5");
}
}
if(filters.get("race") != null) {
if(!ERace.contains(filters.get("race").toUpperCase()))
throw new ValidationException("Race value " + filters.get("race") + " not allowed. Races allowed are " + ERace.valuesToString());
}
if(filters.get("birthday") != null) {
try {
Date.valueOf(filters.get("birthday"));
} catch(IllegalArgumentException e) {
throw new ValidationException("Date value " + filters.get("birthday") + " not allowed. The birthday must be a valid date");
}
}
}
}

View File

@ -17,9 +17,11 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.File;
import java.sql.Date;
import java.util.List;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@ -33,6 +35,98 @@ public class HorseEndpointTest {
@LocalServerPort
private int port;
@Test
@DisplayName("Searching for all horses with no filters, given two horses should return HTTP 200 and both horses")
public void searchingHorses_noFiltersGivenTwoHorses_shouldReturnStatus200AndHorses() {
// Create the horses
HorseDto newHorse = new HorseDto("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0));
HttpEntity<HorseDto> request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> firstHorse = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class);
newHorse = new HorseDto("Katrina", "Not a fast enough horse", (short) 3, Date.valueOf("2005-01-01"), ERace.PAINT, "files/example.png", Long.valueOf(0));
request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> secondHorse = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class);
// Get all horses and save in list
// https://stackoverflow.com/a/31947188
ResponseEntity<List<HorseDto>> allHorses = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.GET, null, new ParameterizedTypeReference<List<HorseDto>>() {});
assertEquals(allHorses.getStatusCode(), HttpStatus.OK);
assertTrue(allHorses.getBody().contains(firstHorse.getBody()));
assertTrue(allHorses.getBody().contains(secondHorse.getBody()));
}
@Test
@DisplayName("Searching for all horses with correct filters, given two horses should return HTTP 200 and one horse")
public void searchingHorses_withCorrectFiltersGivenTwoHorses_shouldReturnStatus200AndHorse() {
// Create the horses
HorseDto newHorse = new HorseDto("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0));
HttpEntity<HorseDto> request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> firstHorse = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class);
newHorse = new HorseDto("Katrina", "Not a fast enough horse", (short) 3, Date.valueOf("2005-01-01"), ERace.PAINT, "files/example.png", Long.valueOf(0));
request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> secondHorse = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class);
// Get all horses and save in list
// https://stackoverflow.com/a/25434451
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(BASE_URL + port + HORSE_URL)
.queryParam("name", "Zephyr")
.queryParam("description", "Nice")
.queryParam("score", "4")
.queryParam("birthday", "2020-03-23")
.queryParam("race", ERace.APPALOOSA.name());
ResponseEntity<List<HorseDto>> allHorses = REST_TEMPLATE.exchange(
builder.toUriString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<HorseDto>>() {}
);
assertEquals(allHorses.getStatusCode(), HttpStatus.OK);
assertTrue(allHorses.getBody().contains(firstHorse.getBody()));
}
@Test
@DisplayName("Searching for all horses with incorrect filters, given two horses should return HTTP 400")
public void searchingHorses_withIncorrectFiltersGivenTwoHorses_shouldReturnStatus400() {
// Create the horses
HorseDto newHorse = new HorseDto("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0));
HttpEntity<HorseDto> request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> firstHorse = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class);
newHorse = new HorseDto("Katrina", "Not a fast enough horse", (short) 3, Date.valueOf("2005-01-01"), ERace.PAINT, "files/example.png", Long.valueOf(0));
request = new HttpEntity<>(newHorse);
ResponseEntity<HorseDto> secondHorse = REST_TEMPLATE
.exchange(BASE_URL + port + HORSE_URL, HttpMethod.POST, request, HorseDto.class);
// Get all horses and save in list
// https://stackoverflow.com/a/25434451
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(BASE_URL + port + HORSE_URL)
.queryParam("name", "Zephyr")
.queryParam("description", "Nice")
.queryParam("score", "10")
.queryParam("birthday", "test")
.queryParam("race", "test");
assertThrows(HttpClientErrorException.BadRequest.class, () ->
REST_TEMPLATE
.exchange(builder.toUriString(), HttpMethod.GET, null, new ParameterizedTypeReference<HorseDto>() {}));
}
@Test
@DisplayName("Adding a new horse with the correct parameters will return HTTP 201 and the new HorseDto")
public void addingNewHorse_correctParameters_shouldReturnStatus201AndHorse() {

View File

@ -13,12 +13,65 @@ import org.springframework.dao.DataAccessException;
import java.io.IOException;
import java.sql.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class HorseDaoTestBase {
@Autowired
HorseDao horseDao;
@Test
@DisplayName("Getting all horses given two horses should return a list with the horses")
public void gettingAllHorses_givenTwoHorses_shouldReturnHorses() {
// Create the horses
Horse firstHorse = horseDao.addHorse(new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0)));
Horse secondHorse = horseDao.addHorse(new Horse("Katrina", "Bad horse", (short) 1, Date.valueOf("2005-01-01"), ERace.APPALOOSA, "files/example.png", Long.valueOf(0)));
// Test if the horses are present
List<Horse> allHorses = horseDao.getAll();
assertTrue(allHorses.contains(firstHorse));
assertTrue(allHorses.contains(secondHorse));
}
@Test
@DisplayName("Searching all horses with correct filters given two horses should return a list with the horse")
public void searchingHorses_withCorrectFiltersGivenTwoHorses_shouldReturnHorse() {
// Create the horses
Horse firstHorse = horseDao.addHorse(new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0)));
Horse secondHorse = horseDao.addHorse(new Horse("Katrina", "Bad horse", (short) 1, Date.valueOf("2005-01-01"), ERace.APPALOOSA, "files/example.png", Long.valueOf(0)));
// Test if the horses are present
Map<String, String> filters = new HashMap<String, String>();
filters.put("name", "Zephyr");
filters.put("description", "Nice");
filters.put("score", "4");
filters.put("birthday", "2020-03-23");
filters.put("race", ERace.APPALOOSA.name());
List<Horse> allHorses = horseDao.getFiltered(filters);
assertTrue(allHorses.contains(firstHorse));
}
@Test
@DisplayName("Searching all horses with incorrect filters given two horses should throw NotFoundException")
public void searchingHorses_withIncorrectFiltersGivenTwoHorses_shouldThrowNotFound() {
// Create the horses
Horse firstHorse = horseDao.addHorse(new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0)));
Horse secondHorse = horseDao.addHorse(new Horse("Katrina", "Bad horse", (short) 1, Date.valueOf("2005-01-01"), ERace.APPALOOSA, "files/example.png", Long.valueOf(0)));
// Test if the horses are present
Map<String, String> filters = new HashMap<String, String>();
filters.put("name", "Tester Horse");
filters.put("description", "A horse for testing");
filters.put("score", "1");
filters.put("birthday", "2020-02-01");
filters.put("race", ERace.APPALOOSA.name());
assertThrows(NotFoundException.class, () -> horseDao.getFiltered(filters));
}
@Test
@DisplayName("Adding a new horse with the correct parameters should return the horse")
public void addingNewHorse_correctParameters_shouldReturnHorse() {
@ -101,5 +154,4 @@ public abstract class HorseDaoTestBase {
public void deletingHorse_nonexistent_shouldThrowNotFound() throws IOException {
assertThrows(NotFoundException.class, () -> horseDao.deleteHorse(null));
}
}

View File

@ -16,6 +16,9 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.IOException;
import java.sql.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -88,4 +91,50 @@ public class HorseServiceTest {
newHorse.setOwner(null);
assertThrows(ValidationException.class, () -> horseService.updateHorse(newHorse));
}
@Test
@DisplayName("Getting all horses given two horses should return a list with the horses")
public void gettingAllHorses_givenTwoHorses_shouldReturnHorses() {
// Create the horses
Horse firstHorse = horseService.addHorse(new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0)));
Horse secondHorse = horseService.addHorse(new Horse("Katrina", "Bad horse", (short) 1, Date.valueOf("2005-01-01"), ERace.APPALOOSA, "files/example.png", Long.valueOf(0)));
// Test if the horses are present
List<Horse> allHorses = horseService.getAll();
assertTrue(allHorses.contains(firstHorse));
assertTrue(allHorses.contains(secondHorse));
}
@Test
@DisplayName("Searching all horses with correct filters given two horses should return a list with the horse")
public void searchingHorses_withCorrectFiltersGivenTwoHorses_shouldReturnHorse() {
// Create the horses
Horse firstHorse = horseService.addHorse(new Horse("Zephyr", "Nice horse", (short) 4, Date.valueOf("2020-01-01"), ERace.APPALOOSA, "files/test.png", Long.valueOf(0)));
Horse secondHorse = horseService.addHorse(new Horse("Katrina", "Bad horse", (short) 1, Date.valueOf("2005-01-01"), ERace.APPALOOSA, "files/example.png", Long.valueOf(0)));
// Test if the horses are present
Map<String, String> filters = new HashMap<String, String>();
filters.put("name", "Zephyr");
filters.put("description", "Nice");
filters.put("score", "4");
filters.put("birthday", "2020-03-23");
filters.put("race", ERace.APPALOOSA.name());
List<Horse> allHorses = horseService.getFiltered(filters);
assertTrue(allHorses.contains(firstHorse));
}
@Test
@DisplayName("Searching all horses with incorrect filters given no horses should throw ValidationException")
public void searchingHorses_withIncorrectFiltersGivenNoHorses_shouldThrowValidation() {
// Test for exception
Map<String, String> filters = new HashMap<String, String>();
filters.put("name", "Tester Horse");
filters.put("description", "A horse for testing");
filters.put("score", "10");
filters.put("birthday", "test");
filters.put("race", "yes");
assertThrows(ValidationException.class, () -> horseService.getFiltered(filters));
}
}