Browse Source

refacto error handling & add locale validation mechanism

master
art.dambrine 4 years ago
parent
commit
d510cdf999
  1. 37
      TODO.md
  2. 50
      src/main/java/com/example/apispringgradleb2boost/controller/PartnerController.java
  3. 31
      src/main/java/com/example/apispringgradleb2boost/exceptionhandling/CustomError.java
  4. 36
      src/main/java/com/example/apispringgradleb2boost/exceptionhandling/GeneralExceptionHandler.java
  5. 8
      src/main/java/com/example/apispringgradleb2boost/model/Partner.java
  6. 52
      src/main/java/com/example/apispringgradleb2boost/service/PartnerService.java

37
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)

50
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> 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<Partner> 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> 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();
}
}

31
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 + "}";
}
}

36
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<Object> handleAll(Exception ex, WebRequest request) {
CustomError customError = new CustomError(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getLocalizedMessage());
return new ResponseEntity<Object>(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<Object>(customError.getMessage(), headers, customError.getCode());
}
@ExceptionHandler({CustomError.class})
public ResponseEntity<Object> handleException(CustomError customError) {
// log exception
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new ResponseEntity<Object>(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<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
CustomError customError = new CustomError(HttpStatus.NOT_FOUND.value(), ex.getLocalizedMessage());
return new ResponseEntity<Object>(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<Object>(customError.getMessage(), headers, customError.getCode());
}
}

8
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;
}

52
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<Partner> 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()))
));
}
}
}

Loading…
Cancel
Save