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. 50
      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.beans.factory.annotation.Autowired;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import com.google.gson.Gson; import com.google.gson.Gson;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional; import java.util.Optional;
@RestController @RestController
@ -44,14 +42,14 @@ public class PartnerController {
* @return A Partner object * @return A Partner object
*/ */
@GetMapping("/partner/{id}") @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); Optional<Partner> partner = partnerService.getPartnerById(Id);
if (partner.isPresent()) { if (partner.isPresent()) {
return partner.get(); return partner.get();
} else { } else {
// Error handling when !partner.isPresent() // Error handling when !partner.isPresent()
handlePartnerResourceIsNotPresentReturnNotFound(Id, response); handlePartnerResourceIsNotPresentReturnNotFound(Id);
return null; return null;
} }
} }
@ -77,32 +75,13 @@ public class PartnerController {
*/ */
@PutMapping("/partner/{id}") @PutMapping("/partner/{id}")
public Partner updatePartner(@PathVariable("id") final Long Id, @RequestBody Partner partner, 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); Optional<Partner> p = partnerService.getPartnerById(Id);
if (p.isPresent()) { if (p.isPresent()) {
Partner currentPartner = p.get(); return partnerService.updatePartner(partner, p);
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;
} else { } else {
handlePartnerResourceIsNotPresentReturnNotFound(Id, response); handlePartnerResourceIsNotPresentReturnNotFound(Id);
return null; return null;
} }
} }
@ -114,35 +93,28 @@ public class PartnerController {
* @param Id - The id of the partner to delete * @param Id - The id of the partner to delete
*/ */
@DeleteMapping("/partner/{id}") @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); Optional<Partner> partner = partnerService.getPartnerById(Id);
if (partner.isPresent()) { if (partner.isPresent()) {
partnerService.deletePartner(Id); partnerService.deletePartner(Id);
} else { } 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 Id
* @param response * @throws CustomError
* @throws IOException
*/ */
private void handlePartnerResourceIsNotPresentReturnNotFound(@PathVariable("id") Long Id, HttpServletResponse response) throws IOException { private void handlePartnerResourceIsNotPresentReturnNotFound(final Long Id) throws CustomError {
// Error handling when !partner.isPresent() // Error handling when !partner.isPresent()
response.setStatus(HttpStatus.NOT_FOUND.value()); throw new CustomError(HttpStatus.NOT_FOUND.value(), new Gson().toJson(
response.setContentType(String.valueOf(MediaType.APPLICATION_JSON));
response.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
out.print(new Gson().toJson(
new CustomError(HttpStatus.NOT_FOUND.value(), new CustomError(HttpStatus.NOT_FOUND.value(),
String.format("Partner with id %d not found!", Id)) 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; 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.Data;
import lombok.EqualsAndHashCode;
@Data @Data
public class CustomError { @EqualsAndHashCode(callSuper = true)
public class CustomError extends RuntimeException {
private int code; private final int code;
private String message; private final String message;
public CustomError(int code, String message) { public CustomError(int code, final String message) {
this.code = code; this.code = code;
this.message = message; 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; package com.example.apispringgradleb2boost.exceptionhandling;
import com.google.gson.Gson;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
@ -15,8 +17,22 @@ public class GeneralExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({Exception.class}) @ExceptionHandler({Exception.class})
public ResponseEntity<Object> handleAll(Exception ex, WebRequest request) { 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 @Override
@ -25,15 +41,23 @@ public class GeneralExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus status, HttpStatus status,
WebRequest request) { WebRequest request) {
CustomError customError = new CustomError(HttpStatus.BAD_REQUEST.value(), ex.getLocalizedMessage()); headers.setContentType(MediaType.APPLICATION_JSON);
return handleExceptionInternal(ex, customError, headers, HttpStatus.valueOf(customError.getCode()), request); 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 @Override
protected ResponseEntity<Object> handleNoHandlerFoundException( protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
CustomError customError = new CustomError(HttpStatus.NOT_FOUND.value(), ex.getLocalizedMessage()); headers.setContentType(MediaType.APPLICATION_JSON);
return new ResponseEntity<Object>(customError, new HttpHeaders(), customError.getCode()); 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 lombok.Data;
import javax.persistence.*; import javax.persistence.*;
import java.util.Locale;
@Data @Data
@Entity @Entity
@ -15,11 +17,13 @@ public class Partner {
@Column(name = "company_name") @Column(name = "company_name")
private String name; private String name;
@Column(name = "ref", unique=true) @Column(name = "ref", unique = true)
private String reference; private String reference;
private String locale; private Locale locale;
@Column(name = "expires") @Column(name = "expires")
private String expirationTime; private String expirationTime;
} }

50
src/main/java/com/example/apispringgradleb2boost/service/PartnerService.java

@ -1,15 +1,21 @@
package com.example.apispringgradleb2boost.service; package com.example.apispringgradleb2boost.service;
import com.example.apispringgradleb2boost.exceptionhandling.CustomError;
import com.example.apispringgradleb2boost.model.Partner; import com.example.apispringgradleb2boost.model.Partner;
import com.example.apispringgradleb2boost.repository.PartnerRepository; import com.example.apispringgradleb2boost.repository.PartnerRepository;
import com.google.gson.Gson;
import lombok.Data; import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional; import java.util.Optional;
@Data @Data
@ -34,10 +40,54 @@ public class PartnerService {
} }
public Partner savePartner(Partner partner) { public Partner savePartner(Partner partner) {
if (localeIsValid(partner.getLocale())) {
return partnerRepository.save(partner); 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) { public void deletePartner(@Min(0) final Long Id) {
partnerRepository.deleteById(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