diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..e8718ca --- /dev/null +++ b/TODO.md @@ -0,0 +1,37 @@ +# TODO List API rest-api-assignment b2boost +Date : 23-24 nov 2021 + +- [x] 1. The endpoint will return custom error JSON messages in the payload, additionally to the standard HTTP response codes similar to this one: + + ``` + { + "code": 404, + "message": "Partner with id 1 not found!" + } + ``` + +- [x] 2. The application will be cleanly layered, by separating control and business logic. For instance, the controller of the endpoint will not contain any business logic, and will limit itself to + + - marshalling data from the http layer to the service layer + - reporting error conditions in the response + - marshalling results back to the http layer, including custom errors + +- [x] 3. The service layer will be transactional and encapsulate all validation and database interactions + +- [x] 4. The data layer will be implemented by using a data repository service + +- [x] 5. The application can run with an embedded in-memory database + +- [ ] 6. The application will have a health check endpoint + +- [ ] 7. The application will have suitable functional tests, checking real http functionality + +- [x] 8. No authentication/security necessary + +- [ ] This document will contain the specification of the REST endpoint, with data definition and error payload specification. + +You should document how to + +- [ ] Run the test suite +- [ ] Run application +- [ ] Optionally, a commentary on how you would deploy it (not necessary to implement this) \ No newline at end of file diff --git a/src/main/java/com/example/apispringgradleb2boost/controller/PartnerController.java b/src/main/java/com/example/apispringgradleb2boost/controller/PartnerController.java index 3857803..3bdff18 100644 --- a/src/main/java/com/example/apispringgradleb2boost/controller/PartnerController.java +++ b/src/main/java/com/example/apispringgradleb2boost/controller/PartnerController.java @@ -7,14 +7,12 @@ import com.example.apispringgradleb2boost.service.PartnerService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.query.Param; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import com.google.gson.Gson; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.PrintWriter; import java.util.Optional; @RestController @@ -44,14 +42,14 @@ public class PartnerController { * @return A Partner object */ @GetMapping("/partner/{id}") - public Partner getPartnerById(@PathVariable("id") final Long Id, HttpServletResponse response) throws IOException { + public Partner getPartnerById(@PathVariable("id") final Long Id, HttpServletResponse response) throws CustomError { Optional partner = partnerService.getPartnerById(Id); if (partner.isPresent()) { return partner.get(); } else { // Error handling when !partner.isPresent() - handlePartnerResourceIsNotPresentReturnNotFound(Id, response); + handlePartnerResourceIsNotPresentReturnNotFound(Id); return null; } } @@ -77,32 +75,13 @@ public class PartnerController { */ @PutMapping("/partner/{id}") public Partner updatePartner(@PathVariable("id") final Long Id, @RequestBody Partner partner, - HttpServletResponse response) throws IOException { + HttpServletResponse response) throws CustomError { Optional p = partnerService.getPartnerById(Id); if (p.isPresent()) { - Partner currentPartner = p.get(); - - String name = partner.getName(); - if (name != null) { - currentPartner.setName(name); - } - String reference = partner.getReference(); - if (reference != null) { - currentPartner.setReference(reference); - } - String locale = partner.getLocale(); - if (locale != null) { - currentPartner.setLocale(locale); - } - String expirationTime = partner.getExpirationTime(); - if (expirationTime != null) { - currentPartner.setExpirationTime(expirationTime); - } - partnerService.savePartner(currentPartner); - return currentPartner; + return partnerService.updatePartner(partner, p); } else { - handlePartnerResourceIsNotPresentReturnNotFound(Id, response); + handlePartnerResourceIsNotPresentReturnNotFound(Id); return null; } } @@ -114,35 +93,28 @@ public class PartnerController { * @param Id - The id of the partner to delete */ @DeleteMapping("/partner/{id}") - public void deletePartner(@PathVariable("id") final Long Id, HttpServletResponse response) throws IOException { + public void deletePartner(@PathVariable("id") final Long Id, HttpServletResponse response) throws CustomError { Optional partner = partnerService.getPartnerById(Id); if (partner.isPresent()) { partnerService.deletePartner(Id); } else { - handlePartnerResourceIsNotPresentReturnNotFound(Id, response); + handlePartnerResourceIsNotPresentReturnNotFound(Id); } } /** - * Partner not found handling - Extraction of duplicated code for + * Partner not found handling - Extraction of duplicated code when !partner.isPresent() * * @param Id - * @param response - * @throws IOException + * @throws CustomError */ - private void handlePartnerResourceIsNotPresentReturnNotFound(@PathVariable("id") Long Id, HttpServletResponse response) throws IOException { + private void handlePartnerResourceIsNotPresentReturnNotFound(final Long Id) throws CustomError { // Error handling when !partner.isPresent() - response.setStatus(HttpStatus.NOT_FOUND.value()); - response.setContentType(String.valueOf(MediaType.APPLICATION_JSON)); - response.setCharacterEncoding("UTF-8"); - - PrintWriter out = response.getWriter(); - out.print(new Gson().toJson( + throw new CustomError(HttpStatus.NOT_FOUND.value(), new Gson().toJson( new CustomError(HttpStatus.NOT_FOUND.value(), String.format("Partner with id %d not found!", Id)) )); - out.flush(); } } diff --git a/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/CustomError.java b/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/CustomError.java index 9525d09..b9e5e07 100644 --- a/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/CustomError.java +++ b/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/CustomError.java @@ -1,16 +1,39 @@ package com.example.apispringgradleb2boost.exceptionhandling; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import lombok.Data; +import lombok.EqualsAndHashCode; @Data -public class CustomError { +@EqualsAndHashCode(callSuper = true) +public class CustomError extends RuntimeException { - private int code; - private String message; + private final int code; + private final String message; - public CustomError(int code, String message) { + public CustomError(int code, final String message) { this.code = code; this.message = message; } + /** + * Overriding getMessage from RuntimeException to avoid printing stackTrace and suppressedExceptions as for ex : + * { + * "code": 404, + * "message": "No handler found for GET /partnersdf", + * "stackTrace": [], + * "suppressedExceptions": [] + * } + * @return overrideMessage + * */ + @Override + public String getMessage() { + Gson gson = new Gson(); + JsonObject body = gson.fromJson(message, JsonObject.class); + JsonElement extractedCode = body.get("code"); + JsonElement extractedMessage = body.get("message"); + return "{\"code\":" + extractedCode + ", \"message\": " + extractedMessage + "}"; + } } diff --git a/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/GeneralExceptionHandler.java b/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/GeneralExceptionHandler.java index 2e21d90..4fd85e6 100644 --- a/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/GeneralExceptionHandler.java +++ b/src/main/java/com/example/apispringgradleb2boost/exceptionhandling/GeneralExceptionHandler.java @@ -1,7 +1,9 @@ package com.example.apispringgradleb2boost.exceptionhandling; +import com.google.gson.Gson; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -15,8 +17,22 @@ public class GeneralExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({Exception.class}) public ResponseEntity handleAll(Exception ex, WebRequest request) { - CustomError customError = new CustomError(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getLocalizedMessage()); - return new ResponseEntity(customError, new HttpHeaders(), customError.getCode()); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + CustomError customError = new CustomError(HttpStatus.INTERNAL_SERVER_ERROR.value(), new Gson().toJson( + new CustomError(HttpStatus.INTERNAL_SERVER_ERROR.value(), + ex.getLocalizedMessage()) + )); + return new ResponseEntity(customError.getMessage(), headers, customError.getCode()); + } + + @ExceptionHandler({CustomError.class}) + public ResponseEntity handleException(CustomError customError) { + // log exception + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + return new ResponseEntity(customError.getMessage(), headers, customError.getCode()); } @Override @@ -25,15 +41,23 @@ public class GeneralExceptionHandler extends ResponseEntityExceptionHandler { HttpStatus status, WebRequest request) { - CustomError customError = new CustomError(HttpStatus.BAD_REQUEST.value(), ex.getLocalizedMessage()); - return handleExceptionInternal(ex, customError, headers, HttpStatus.valueOf(customError.getCode()), request); + headers.setContentType(MediaType.APPLICATION_JSON); + CustomError customError = new CustomError(HttpStatus.BAD_REQUEST.value(), new Gson().toJson( + new CustomError(HttpStatus.BAD_REQUEST.value(), + ex.getLocalizedMessage()) + )); + return handleExceptionInternal(ex, customError.getMessage(), headers, HttpStatus.valueOf(customError.getCode()), request); } @Override protected ResponseEntity handleNoHandlerFoundException( NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { - CustomError customError = new CustomError(HttpStatus.NOT_FOUND.value(), ex.getLocalizedMessage()); - return new ResponseEntity(customError, new HttpHeaders(), customError.getCode()); + headers.setContentType(MediaType.APPLICATION_JSON); + CustomError customError = new CustomError(HttpStatus.NOT_FOUND.value(), new Gson().toJson( + new CustomError(HttpStatus.NOT_FOUND.value(), + ex.getLocalizedMessage()) + )); + return new ResponseEntity(customError.getMessage(), headers, customError.getCode()); } } diff --git a/src/main/java/com/example/apispringgradleb2boost/model/Partner.java b/src/main/java/com/example/apispringgradleb2boost/model/Partner.java index 079c1bf..454e54f 100644 --- a/src/main/java/com/example/apispringgradleb2boost/model/Partner.java +++ b/src/main/java/com/example/apispringgradleb2boost/model/Partner.java @@ -3,6 +3,8 @@ package com.example.apispringgradleb2boost.model; import lombok.Data; import javax.persistence.*; +import java.util.Locale; + @Data @Entity @@ -15,11 +17,13 @@ public class Partner { @Column(name = "company_name") private String name; - @Column(name = "ref", unique=true) + @Column(name = "ref", unique = true) private String reference; - private String locale; + private Locale locale; @Column(name = "expires") private String expirationTime; + + } diff --git a/src/main/java/com/example/apispringgradleb2boost/service/PartnerService.java b/src/main/java/com/example/apispringgradleb2boost/service/PartnerService.java index 336a037..ccd24ac 100644 --- a/src/main/java/com/example/apispringgradleb2boost/service/PartnerService.java +++ b/src/main/java/com/example/apispringgradleb2boost/service/PartnerService.java @@ -1,15 +1,21 @@ package com.example.apispringgradleb2boost.service; +import com.example.apispringgradleb2boost.exceptionhandling.CustomError; import com.example.apispringgradleb2boost.model.Partner; import com.example.apispringgradleb2boost.repository.PartnerRepository; +import com.google.gson.Gson; import lombok.Data; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.validation.constraints.Min; +import java.util.Arrays; +import java.util.Locale; import java.util.Optional; @Data @@ -34,10 +40,54 @@ public class PartnerService { } public Partner savePartner(Partner partner) { - return partnerRepository.save(partner); + if (localeIsValid(partner.getLocale())) { + return partnerRepository.save(partner); + } else { + return null; + } + } + + public Partner updatePartner(Partner partner, Optional p) { + Partner currentPartner = p.get(); + + String name = partner.getName(); + if (name != null) { + currentPartner.setName(name); + } + String reference = partner.getReference(); + if (reference != null) { + currentPartner.setReference(reference); + } + Locale locale = partner.getLocale(); + if (localeIsValid(locale)) { + currentPartner.setLocale(locale); + } + String expirationTime = partner.getExpirationTime(); + if (expirationTime != null) { + currentPartner.setExpirationTime(expirationTime); + } + savePartner(currentPartner); + return currentPartner; } public void deletePartner(@Min(0) final Long Id) { partnerRepository.deleteById(Id); } + + /** + * Check whether the locale object is valid locale if not throw custom error + * + * @param locale + * @return Boolean + */ + public Boolean localeIsValid(Locale locale) { + if (Arrays.asList(Locale.getAvailableLocales()).contains(locale)) { + return true; + } else { + throw new CustomError(HttpStatus.BAD_REQUEST.value(), new Gson().toJson( + new CustomError(HttpStatus.BAD_REQUEST.value(), + String.format("Locale %s is an invalid Locale!", locale.toString())) + )); + } + } }