diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java old mode 100644 new mode 100755 diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar old mode 100644 new mode 100755 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties old mode 100644 new mode 100755 diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..57b4014 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java -jar target/clickbus-0.0.1-SNAPSHOT.jar \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index c9b6851..96cfa5b --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# Desafio que resolve outro desafio +- Estou participando de um bootcamp do Santander na DIO, e como desafio, +foi proposto o desenvolvimento e publicação de uma API RESTFULL. +- Encontrei o desafio da @Clickbus e achei que seria interessante +entender e melhorar este desafio de código proposto pela empresa. + # Backend Developer Challenge Esse desafio serve para avaliarmos suas habilidades em construir APIs. diff --git a/docker-compose.yml b/docker-compose.yml old mode 100644 new mode 100755 index a788dcf..6890a04 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,9 @@ services: container_name: clickbus_challenge_app restart: always image: maven:3.3-jdk-8 - build: . + build: + context: ./docker/app/ + dockerfile: Dockerfile working_dir: /app volumes: - ./:/app diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100755 index 0000000..d655234 --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,13 @@ +# Stage 1: Build the application +FROM maven:3.8.6-jdk-8 as build +WORKDIR /home/app +COPY ../../src /home/app/src +COPY ../../pom.xml /home/app +RUN mvn clean package + +# Stage 2: Create the final image +FROM openjdk:8-jre +WORKDIR /home/app +COPY --from=build /home/app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index cf21f4f..8c603bc 100755 --- a/pom.xml +++ b/pom.xml @@ -28,11 +28,13 @@ org.springframework.boot spring-boot-starter-web + 2.2.1.RELEASE org.springframework.boot spring-boot-starter-data-jpa + 2.4.0 @@ -41,15 +43,10 @@ runtime - - com.h2database - h2 - runtime - - org.projectlombok lombok + 1.18.30 true @@ -95,6 +92,7 @@ h2 runtime + org.apache.commons commons-lang3 diff --git a/src/main/java/br/com/clickbus/challenge/ClickbusApplication.java b/src/main/java/br/com/clickbus/challenge/ClickbusApplication.java old mode 100644 new mode 100755 diff --git a/src/main/java/br/com/clickbus/challenge/controller/PlaceController.java b/src/main/java/br/com/clickbus/challenge/controller/PlaceController.java old mode 100644 new mode 100755 index bbd5d5c..d7e08c0 --- a/src/main/java/br/com/clickbus/challenge/controller/PlaceController.java +++ b/src/main/java/br/com/clickbus/challenge/controller/PlaceController.java @@ -6,25 +6,24 @@ import br.com.clickbus.challenge.exception.PlaceNotFoundException; import br.com.clickbus.challenge.service.PlaceService; import io.swagger.annotations.Api; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; @Api("places") @RestController @RequestMapping("places") public class PlaceController { - private PlaceService service; + private final PlaceService service; + + public PlaceController(PlaceService service) { + this.service = service; + } @PostMapping public ResponseEntity create(@RequestBody @Valid PlaceDTO dto) { @@ -35,7 +34,21 @@ public ResponseEntity create(@RequestBody @Valid PlaceDTO dto) { public ResponseEntity findById(@PathVariable Long id) { return service.findById(id) .map(place -> ResponseEntity.ok(place.convertToDTO())) - .orElseThrow(() -> new PlaceNotFoundException(HttpStatus.NOT_FOUND)); + .orElseThrow(() -> new PlaceNotFoundException(HttpStatus.NOT_FOUND.toString())); + } + + @GetMapping(name = "findByName", path = "/") + public ResponseEntity> findByName(@RequestParam(value = "name") String name) { + List places = service.findByName(name); + + if (places.isEmpty()) { + throw new PlaceNotFoundException("There are no places associated with the given name."); + } + + return ResponseEntity.ok(service.findByName(name) + .stream() + .map(Place::convertToDTO) + .collect(Collectors.toList())); } @GetMapping diff --git a/src/main/java/br/com/clickbus/challenge/dto/PlaceDTO.java b/src/main/java/br/com/clickbus/challenge/dto/PlaceDTO.java old mode 100644 new mode 100755 diff --git a/src/main/java/br/com/clickbus/challenge/entity/Place.java b/src/main/java/br/com/clickbus/challenge/entity/Place.java old mode 100644 new mode 100755 index ccfc550..6576164 --- a/src/main/java/br/com/clickbus/challenge/entity/Place.java +++ b/src/main/java/br/com/clickbus/challenge/entity/Place.java @@ -41,7 +41,7 @@ public class Place { public Place(String name, String slug, String city, String state) { this.name = name; - this.slug = slug; + this.slug = Place.slugfy(slug); this.city = city; this.state = state; this.createdAt = LocalDateTime.now(); @@ -51,7 +51,23 @@ public static Place of(String name, String slug, String city, String state) { return new Place(name, slug, city, state); } + private static String slugfy(String slug) { + return slug.toLowerCase() + .replaceAll("[^a-zA-Z0-9\\s+]", "") + .replaceAll("\\s+", "-"); + } + public PlaceDTO convertToDTO() { return PlaceDTO.of(this.name, this.slug, this.city, this.state); } + + public Place alter(PlaceDTO dto) { + this.name = dto.getName(); + this.slug = Place.slugfy(dto.getSlug()); + this.city = dto.getCity(); + this.state = dto.getState(); + this.updatedAt = LocalDateTime.now(); + + return this; + } } diff --git a/src/main/java/br/com/clickbus/challenge/exception/GlobalExceptionHandler.java b/src/main/java/br/com/clickbus/challenge/exception/GlobalExceptionHandler.java new file mode 100755 index 0000000..bcfafcf --- /dev/null +++ b/src/main/java/br/com/clickbus/challenge/exception/GlobalExceptionHandler.java @@ -0,0 +1,54 @@ +package br.com.clickbus.challenge.exception; + +import org.apache.commons.lang3.NotImplementedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(NotImplementedException.class) + public ResponseEntity handleNotImplementedException(NotImplementedException notImplementedException) { + String message = notImplementedException.getMessage().isEmpty() + ? "Resource not implemented yet." + : notImplementedException.getMessage(); + return new ResponseEntity<>(this.responseError(message, HttpStatus.NOT_IMPLEMENTED), HttpStatus.NOT_IMPLEMENTED); + } + + @ExceptionHandler(PlaceNotFoundException.class) + public ResponseEntity handlePlaceNotFoundException(PlaceNotFoundException placeNotFoundException) { + String message = placeNotFoundException.getMessage().isEmpty() + ? "Resource ID not found." + : placeNotFoundException.getMessage(); + return new ResponseEntity<>(this.responseError(message, HttpStatus.NOT_FOUND), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleBusinessException(MethodArgumentNotValidException methodArgumentNotValidException) { + String message = methodArgumentNotValidException.getMessage().isEmpty() + ? HttpStatus.BAD_REQUEST.toString() + : methodArgumentNotValidException.getMessage(); + return new ResponseEntity<>(this.responseError(message, HttpStatus.BAD_REQUEST), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Throwable.class) + public ResponseEntity handleUnexpectedException(Throwable unexpectedException) { + String message = "Unexpected server error."; + logger.error("", unexpectedException); + return new ResponseEntity<>(this.responseError(message, HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR); + } + + private ResponseErrorBody responseError(String message, HttpStatus statusCode){ + ResponseErrorBody responseError = new ResponseErrorBody(); + responseError.setStatus("error"); + responseError.setError(message); + responseError.setStatusCode(statusCode.value()); + return responseError; + } +} diff --git a/src/main/java/br/com/clickbus/challenge/exception/PlaceNotFoundException.java b/src/main/java/br/com/clickbus/challenge/exception/PlaceNotFoundException.java old mode 100644 new mode 100755 index d96301d..6047b9b --- a/src/main/java/br/com/clickbus/challenge/exception/PlaceNotFoundException.java +++ b/src/main/java/br/com/clickbus/challenge/exception/PlaceNotFoundException.java @@ -1,15 +1,9 @@ package br.com.clickbus.challenge.exception; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) public class PlaceNotFoundException extends RuntimeException{ - private String message; - - public PlaceNotFoundException(HttpStatus status){ - super(status.toString()); + public PlaceNotFoundException(String message){ + super(message); } } diff --git a/src/main/java/br/com/clickbus/challenge/exception/ResponseErrorBody.java b/src/main/java/br/com/clickbus/challenge/exception/ResponseErrorBody.java new file mode 100644 index 0000000..eb8499a --- /dev/null +++ b/src/main/java/br/com/clickbus/challenge/exception/ResponseErrorBody.java @@ -0,0 +1,13 @@ +package br.com.clickbus.challenge.exception; + +import lombok.Data; + +import java.util.Date; + +@Data +public class ResponseErrorBody { + private Date timestamp = new Date(); + private String status = "error"; + private int statusCode = 400; + private String error; +} diff --git a/src/main/java/br/com/clickbus/challenge/repository/PlaceRepository.java b/src/main/java/br/com/clickbus/challenge/repository/PlaceRepository.java old mode 100644 new mode 100755 diff --git a/src/main/java/br/com/clickbus/challenge/service/PlaceService.java b/src/main/java/br/com/clickbus/challenge/service/PlaceService.java old mode 100644 new mode 100755 index 799d1a8..6f612f1 --- a/src/main/java/br/com/clickbus/challenge/service/PlaceService.java +++ b/src/main/java/br/com/clickbus/challenge/service/PlaceService.java @@ -5,36 +5,37 @@ import br.com.clickbus.challenge.entity.Place; import br.com.clickbus.challenge.repository.PlaceRepository; import lombok.AllArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.validation.constraints.NotNull; import java.util.List; import java.util.Optional; -import org.apache.commons.lang3.NotImplementedException; @Service @AllArgsConstructor public class PlaceService { - private PlaceRepository repository; + @Autowired + private PlaceRepository placeRepository; public List findAll() { - throw new NotImplementedException("Metodo nao implementado"); + return placeRepository.findAll(); } public Optional findById(@NotNull Long id) { - throw new NotImplementedException("Metodo nao implementado"); + return placeRepository.findById(id); } - public Place save(@NotNull Place place) { - throw new NotImplementedException("Metodo nao implementado"); + public Place save(Place place) { + return placeRepository.save(place); } public List findByName(@NotNull String name) { - throw new NotImplementedException("Metodo nao implementado"); + return placeRepository.findByName(name); } - public Place alter(@NotNull Place place,@NotNull PlaceDTO placeDTO) { - throw new NotImplementedException("Metodo nao implementado"); + public Place alter(@NotNull Place place, @NotNull PlaceDTO placeDTO) { + return placeRepository.save(place.alter(placeDTO)); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml old mode 100644 new mode 100755 index 9bd4fe5..7fe46be --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,6 +7,8 @@ spring: console: enabled: true path: /h2 + settings: + web-allow-others: true datasource: url: jdbc:h2:mem:testdb username: sa diff --git a/src/test/java/br/com/clickbus/challenge/contoller/PlaceControllerTest.java b/src/test/java/br/com/clickbus/challenge/contoller/PlaceControllerTest.java old mode 100644 new mode 100755 diff --git a/src/test/java/br/com/clickbus/challenge/service/PlaceServiceTest.java b/src/test/java/br/com/clickbus/challenge/service/PlaceServiceTest.java old mode 100644 new mode 100755 index cf0f0a9..4adc293 --- a/src/test/java/br/com/clickbus/challenge/service/PlaceServiceTest.java +++ b/src/test/java/br/com/clickbus/challenge/service/PlaceServiceTest.java @@ -11,7 +11,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -19,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.atLeastOnce; @@ -45,6 +43,26 @@ void setUp() { place = Place.of(NAME_PLACE, "bt", "Sao Paulo", "SP"); } + @Test + void whenFindAllNotEmpty() { + when(repository.findAll()).thenReturn(Collections.singletonList(place)); + + List places = service.findAll(); + + assertEquals(1, places.size()); + verify(repository, atLeastOnce()).findAll(); + } + + @Test + void whenFindAllEmpty() { + when(repository.findAll()).thenReturn(Collections.emptyList()); + + List places = service.findAll(); + + assertEquals(0, places.size()); + verify(repository, atLeastOnce()).findAll(); + } + @Test void whenFindByIdOk() { when(repository.findById(1L)).thenReturn(Optional.of(place)); @@ -60,7 +78,6 @@ void whenFindByIdOk() { verify(repository, atLeastOnce()).findById(anyLong()); } - @Test void whenFindByIdThenReturnEmpty() { when(repository.findById(1L)).thenReturn(Optional.empty());