Spring map Enum in #RequestBody - java

In my controller I made an endpoint that allows status change:
#RequestMapping(value = "{ids}" + "/" + "status", method = RequestMethod.PUT)
public ResponseEntity<Void> changeStatus(#PathVariable final List<Integer> ids,
#NotNull #RequestBody final String status) {
deviceService.updateStatus(ids, status);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
And enum looks like this:
public enum DeviceStatus {
ACTIVE, INACTIVE, DELETED, ARCHIVED;
#JsonCreator
public static DeviceStatus parseWithValidation(String status) {
final String upperCaseStatus = status.toUpperCase();
if (exists(upperCaseStatus)) {
return DeviceStatus.valueOf(upperCaseStatus);
} else {
throw new UnsupportedStatusException();
}
}
private static boolean exists(final String upperCaseStatus) {
return Arrays.stream(values()).anyMatch(c -> c.name().equals(upperCaseStatus));
}
}
But Device domain object has a field Status of type DeviceStatus, so how should change status:
public void updateStatus(final List<Integer> ids, final String status) {
getByIds(ids).forEach(device -> {
device.setStatus(status);
update(device);
});
}
But there is a problem with device.setStatus(status);. I can use parseWithValidation but it doesn't make sense, because it is already done. Someone gives me {"status":"INACTIVE"} How should I parse this enum ?

EDIT: updated see comments
Your request body is an object with one field named status of type DeviceStatus, so you can probably use your Device class
So:
class Device {
// will be validated in the controller
private String status;
// getter, setter, etc
}
// with:
public enum DeviceStatus {
ACTIVE, INACTIVE, DELETED, ARCHIVED;
}
and #RequestBody Foo foo in the controller method signature:
public ResponseEntity<Void> changeStatus(#PathVariable final List<Integer> ids, #NotNull #RequestBody final Device device) {
try {
deviceService.updateStatus(ids, DeviceStatus.valueOf(device.getStatus()));
} catch(IllegalArgumentException ex) {
// throw some custom exception. device.getStatus() was invalid
} catch(NullPointerException ex) {
// throw some custom exception. device.getStatus() was null
}
// ...

Related

How to post data from postman using a static list and also get all data

I am new to spring boot, I want to post the data(let's say question data) using a static list without using database and repository, but i can't understand how to solve it.
This my controller class
#PostMapping
public ResponseEntity<Object> createQuestion(#Valid #RequestBody CreateQuestionCommand command) {
var query = new FindQuestionByIdTemp(command.getId());
var response = queryGateway.query(query, ResponseTypes.instanceOf(Object.class)).join();
if (response != null) {
return new ResponseEntity<>(new QuestionCreatedResponse(command.getId(),"This id is already associated with other question"), HttpStatus.INTERNAL_SERVER_ERROR);
}
try {
commandGateway.send(command);
return new ResponseEntity<>(new QuestionCreatedResponse(command.getId(), "Question Created successfully!"), HttpStatus.CREATED);
} catch (Exception e) {
var safeErrorMessage = "Error while processing create question request for id - " + command.getId();
System.out.println(e.toString());
return new ResponseEntity<>(new QuestionCreatedResponse(command.getId(), safeErrorMessage), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
CreateQuestionCommandClass
public class CreateQuestionCommand {
#AggregateIdentifier
private String id;
private Questions question;
}
Questions Class
public class Questions {
#Id
private String id;
private String title;
private String label;
private String feedback;
}
This is an AggregateClass
#Aggregate
public class QuestionAggregateClass {
#AggregateIdentifier
private String id;
private Questions questions;
public QuestionAggregateClass() {
}
#CommandHandler
public QuestionAggregateClass(CreateQuestionCommands command) {
var newQuestion=command.getQuestions();
newQuestion.setId(command.getId());
var event = QuestionCreatedEvents.builder()
.id(command.getId())
.questions(newQuestion)
.build();
AggregateLifecycle.apply(event);
}
#EventSourcingHandler
public void on(QuestionCreatedEvents event){
this.id=event.getId();
this.questions=event.getQuestions();
}
}
Here i want to make a static list so that when i post data from postman, it will save in that static list and i am able to getData from here also. But i dont know how to make that fucnction while using lambda expression. I dont want to write dummy data manually here, i want to store it from postman sending post request
public class QuestionDataAsList {
public static List<Questions> questions;
public static void save(Questions questions) {
}
}
And want to make implementation of event here, and want to call static list here.
#Component
public class QuestionEventHandlerImp implements QuestionEventHandler{
#Override
public void on(QuestionCreatedEvents event) {
QuestionDataAsList.save(event.getQuestions());
}

How to handle different schema for errors in Retrofit 2?

I have an API which returns one of the following schemas:
Success (data found)
{
item_count: 83,
items_per_page: 25,
offset: 25,
items: [
{ ... },
{ ... },
{ ... },
...
]
}
Failure (no data found)
{
success: false,
error: {
code: 200,
message: "Server is busy"
}
}
I want to use Retrofit 2 with GSON to build a wrapper around this API and convert to POJOs, however I'm uncertain how to handle the fact that the API potentially returns two entirely different schemas. For now, I have the following classes:
public class PaginatedResponse<T> {
private int item_count;
private int items_per_page;
private int offset;
private List<T> items;
public PaginatedResponse<T>(int item_count, int items_per_page, int offset, List<T> items) {
this.item_count = item_count;
this.items_per_page = items_per_page;
this.offset = offset;
this.items = items;
}
public List<T> getItems() {
return this.items;
}
}
public class Item {
private int id;
private String name;
// ...
}
Then for my API interface I have:
public interface API {
#GET("items")
Call<PaginatedResponse<Item>> getItems();
}
Then finally to use this I tried to say:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://localhost")
.addConverterFactory(GsonConverterFactory.create())
.build();
API api = retrofit.create(API.class);
api.getItems().enqueue(new retrofit2.Callback<PaginatedResponse<Broadcast>>() {
#Override
public void onResponse(Call<PaginatedResponse<Broadcast>> call, Response<PaginatedResponse<Broadcast>> response) {
Log.d("SUCCESS", response.body().getItems().toString());
}
#Override
public void onFailure(Call<PaginatedResponse<Broadcast>> call, Throwable t) {
Log.d("FAILURE", t.toString());
}
}
So long as no errors are thrown, this seems to work. But when an error is thrown, I get the following in Logcat and my app crashes:
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference
It seems like because the failure JSON lacks an items property, it's setting List<Item> items to null
It seems like because the failure JSON lacks an items property, it's setting List items to null
Yes, It is. You're getting NullPointerException , because you called toString() on a null object. This is an expected behavior.
Solution
As you have different schema for error and success, you need to create a model with both values. Below given a minimal example,
ResponseModel.java
class ResponseModel {
// ERROR
private final Boolean success;
private final Error error;
// SUCCESS
private final int item_count;
// more success values...
ResponseModel(Boolean success, Error error, int item_count) {
this.success = success;
this.error = error;
this.item_count = item_count;
}
public class Error {
private final int code;
private final String message;
private Error(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
public Boolean getSuccess() {
return success;
}
public Error getError() {
return error;
}
public int getItem_count() {
return item_count;
}
}
and in onResponse method, you can check if the response is success or not like this
ResponseModel responseModel = response.body();
if (responseModel.getError() == null) {
// success
doSomethingWithSuccess(responseModel.getItem_count())
} else {
// error
doSomethingWithError(responseModel.getError())
}

Retrofit returning null response Android

I'm using Retrofit to make API call, When I handle the response I get the next error (Need to get the data from the API call) -
Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference
I don't know if I'm doing it right. Anyway here's my code.
Here's the url link: https://data.gov.il/api/
Retrofit call -
#GET("datastore_search?resource_id=2c33523f-87aa-44ec-a736-edbb0a82975e")
Call<Result> getRecords();
Retrofit base call -
private static Retrofit retrofit;
public static final String BASE_URL = "https://data.gov.il/api/action/";
public static Retrofit getRetrofitInstance() {
if (retrofit == null) {
retrofit = new retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
Model class -
public class Result {
#SerializedName("include_total")
#Expose
private Boolean includeTotal;
#SerializedName("resource_id")
#Expose
private String resourceId;
#SerializedName("fields")
#Expose
private List<Field> fields = null;
#SerializedName("records_format")
#Expose
private String recordsFormat;
#SerializedName("records")
#Expose
private List<Record> records = null;
#SerializedName("limit")
#Expose
private Integer limit;
#SerializedName("_links")
#Expose
private Links links;
#SerializedName("total")
#Expose
private Integer total;
public Boolean getIncludeTotal() {
return includeTotal;
}
public void setIncludeTotal(Boolean includeTotal) {
this.includeTotal = includeTotal;
}
public String getResourceId() {
return resourceId;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public List<Field> getFields() {
return fields;
}
public void setFields(List<Field> fields) {
this.fields = fields;
}
public String getRecordsFormat() {
return recordsFormat;
}
public void setRecordsFormat(String recordsFormat) {
this.recordsFormat = recordsFormat;
}
public List<Record> getRecords() {
return records;
}
public void setRecords(List<Record> records) {
this.records = records;
}
...
Main Activity -
RecallService service = RetrofitClientInstance.getRetrofitInstance().create(RecallService.class);
Call<Result> records = service.getRecords();
records.enqueue(new Callback<Result>() {
#Override
public void onResponse(Call<Result> call, Response<Result> response) {
Log.d(TAG, String.valueOf(response.body().getRecords().get(0).getId())); // ERROR
}
#Override
public void onFailure(Call<Result> call, Throwable t) {
Log.e(TAG, t.getMessage());
}
});
The response, which you are getting from the API, doesn't fit the Result POJO.
The response you get from API is like below:
{
"help": "https://data.gov.il/api/3/action/help_show?name=datastore_search",
"success": true,
"result": {...}
}
By using the Result POJO, you are assuming that you get the response as below, which is a json inside the actual response json, and is not what you actually receive. So, just create a POJO which fairly represents the actual response.
{
"include_total": true,
"resource_id": "2c33523f-87aa-44ec-a736-edbb0a82975e",
"fields": [...],
"records_format": "objects",
"records":[...]
}
Try making a class like below (set the annotations yourself):
class Resp{
Result result;
}
Replace the class Result with Resp, like below and other usages:
#GET("datastore_search?resource_id=2c33523f-87aa-44ec-a736-edbb0a82975e")
Call<Resp> getRecords();
Then, finally you can do:
response.body().getResult().getRecords()
The API link you've shared returns the response in the format below:
{"help": "https://data.gov.il/api/3/action/help_show?name=datastore_search", "success": true, "result": {...}}
You are setting the response object to be of type Result which is actually a sub-element within the root element help in the json response. response.body() would include help and the result would be it's sub-element. Since it is not parsed correctly, you're getting a null response.
You will need to include the root element in your model class and update the API call to use that class type as the response type.

#RequestBody #Valid SomeDTO has field of enum type, custom error message

I have the following #RestController
#RequestMapping(...)
public ResponseEntity(#RequestBody #Valid SomeDTO, BindingResult errors) {
//do something with errors if validation error occur
}
public class SomeDTO {
public SomeEnum someEnum;
}
If the JSON request is { "someEnum": "valid value" }, everything works fine. However, if the request is { "someEnum": "invalid value" }, it only return error code 400.
How can I trap this error so I can provide a custom error message, such as "someEnum must be of value A/B/C".
The answer provided by #Amit is good and works. You can go ahead with that if you want to deserialize an enum in a specific way. But that solution is not scalable. Because every enum which needs validation must be annotated with #JsonCreator.
Other answers won't help you beautify the error message.
So here's my solution generic to all the enums in spring web environment.
#RestControllerAdvice
public class ControllerErrorHandler extends ResponseEntityExceptionHandler {
public static final String BAD_REQUEST = "BAD_REQUEST";
#Override
public ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException exception,
HttpHeaders headers, HttpStatus status, WebRequest request) {
String genericMessage = "Unacceptable JSON " + exception.getMessage();
String errorDetails = genericMessage;
if (exception.getCause() instanceof InvalidFormatException) {
InvalidFormatException ifx = (InvalidFormatException) exception.getCause();
if (ifx.getTargetType()!=null && ifx.getTargetType().isEnum()) {
errorDetails = String.format("Invalid enum value: '%s' for the field: '%s'. The value must be one of: %s.",
ifx.getValue(), ifx.getPath().get(ifx.getPath().size()-1).getFieldName(), Arrays.toString(ifx.getTargetType().getEnumConstants()));
}
}
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setTitle(BAD_REQUEST);
errorResponse.setDetail(errorDetails);
return handleExceptionInternal(exception, errorResponse, headers, HttpStatus.BAD_REQUEST, request);
}
}
This will handle all the invalid enum values of all types and provides a better error message for the end user.
Sample output:
{
"title": "BAD_REQUEST",
"detail": "Invalid enum value: 'INTERNET_BANKING' for the field: 'paymentType'. The value must be one of: [DEBIT, CREDIT]."
}
#ControllerAdvice
public static class GenericExceptionHandlers extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(new ErrorDTO().setError(e.getMessage()), HttpStatus.BAD_REQUEST);
}
}
I created a fully functional Spring boot Application with a Test on Bitbucket
You do not need #Valid for enum validation, you can achieve the required response using below code:
Controller Code, StackDTO has an enum PaymentType in it:
#RequestMapping(value = "/reviews", method = RequestMethod.POST)
#ResponseBody
public ResponseEntity<String> add(#RequestBody StackDTO review) {
return new ResponseEntity<String>(HttpStatus.ACCEPTED);
}
Create an exception class, as EnumValidationException
public class EnumValidationException extends Exception {
private String enumValue = null;
private String enumName = null;
public String getEnumValue() {
return enumValue;
}
public void setEnumValue(String enumValue) {
this.enumValue = enumValue;
}
public String getEnumName() {
return enumName;
}
public void setEnumName(String enumName) {
this.enumName = enumName;
}
public EnumValidationException(String enumValue, String enumName) {
super(enumValue);
this.enumValue = enumValue;
this.enumName = enumName;
}
public EnumValidationException(String enumValue, String enumName, Throwable cause) {
super(enumValue, cause);
this.enumValue = enumValue;
this.enumName = enumName;
}
}
I have enum as below, with a special annotation #JsonCreator on a method create
public enum PaymentType {
CREDIT("Credit"), DEBIT("Debit");
private final String type;
PaymentType(String type) {
this.type = type;
}
String getType() {
return type;
}
#Override
public String toString() {
return type;
}
#JsonCreator
public static PaymentType create (String value) throws EnumValidationException {
if(value == null) {
throw new EnumValidationException(value, "PaymentType");
}
for(PaymentType v : values()) {
if(value.equals(v.getType())) {
return v;
}
}
throw new EnumValidationException(value, "PaymentType");
}
}
Finally RestErrorHandler class,
#ControllerAdvice
public class RestErrorHandler {
#ExceptionHandler(HttpMessageNotReadableException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ResponseEntity<ValidationErrorDTO> processValidationIllegalError(HttpMessageNotReadableException ex,
HandlerMethod handlerMethod, WebRequest webRequest) {
EnumValidationException exception = (EnumValidationException) ex.getMostSpecificCause();
ValidationErrorDTO errorDTO = new ValidationErrorDTO();
errorDTO.setEnumName(exception.getEnumName());
errorDTO.setEnumValue(exception.getEnumValue());
errorDTO.setErrorMessage(exception.getEnumValue() + " is an invalid " + exception.getEnumName());
return new ResponseEntity<ValidationErrorDTO>(errorDTO, HttpStatus.BAD_REQUEST);
}
}
ValidationErrorDTO is the dto with setters/getters of enumValue, enumName and errorMessage. Now when you send POST call to controller endpoint /reviews with below request
{"paymentType":"Credit2"}
Then code returns response as 400 with below response body -
{
"enumValue": "Credit2",
"enumName": "PaymentType",
"errorMessage": "Credit2 is an invalid PaymentType"
}
Let me know if it resolves your issue.
Yon can achieve this using #ControllerAdvice as follows
#org.springframework.web.bind.annotation.ExceptionHandler(value = {InvalidFormatException.class})
public ResponseEntity handleIllegalArgumentException(InvalidFormatException exception) {
return ResponseEntity.badRequest().body(exception.getMessage());
}
Basically , the idea is to catch com.fasterxml.jackson.databind.exc.InvalidFormatException and handle it as per your requirement.
#Valid has to do with Hibernate bean validation. Currently enum type is not supported out of the box. I found this answer to be the closet, https://funofprograming.wordpress.com/2016/09/29/java-enum-validator/, the drawback however is that you have to make the enum field of type String instead.

Spring #RequestBody Override

I have a Spring controller that takes posts and it works. The only problem is that our SMS providers will be sending us headers that contain keys with a capitalized first letter, for example:
{
"FromPhoneNumber":"15177754077",
"ToPhoneNumber":"17572046106",
"ResponseReceiveDate":"7/29/2014 5:25:10 AM",
"Message":"PIN 1234"
}
Spring will throw an error like:
Could not read JSON: Unrecognized field "FromPhoneNumber" (class com.talksoft.spring.rest.domain.CDynePost), not marked as ignorable (4 known properties: "responseReceiveDate", "toPhoneNumber", "fromPhoneNumber", "message"])
So, there must be a way for me to override this behavior. Here is the controller method that handles the CDyne posts:
#RequestMapping(method = RequestMethod.POST, value="/celltrust")
public ResponseEntity<String> cellTrustPost(#RequestBody CDynePost cDynePost) {
String message = "FAILED";
UserInteraction userInteraction = getUserInteraction(cDynePost);
boolean success = someSpringService.logMessage(userInteraction);
if (success) {
message = "OK";
return new ResponseEntity<String>(message, HttpStatus.ACCEPTED);
} else {
return new ResponseEntity<String>(message, HttpStatus.FAILED_DEPENDENCY);
}
}
and here is the CDynePost class:
public class CDynePost {
private String FromPhoneNumber;
private String ToPhoneNumber;
private String ResponseReceiveDate;
private String Message;
public String getFromPhoneNumber() {
return FromPhoneNumber;
}
public void setFromPhoneNumber(String FromPhoneNumber) {
this.FromPhoneNumber = FromPhoneNumber;
}
public String getToPhoneNumber() {
return ToPhoneNumber;
}
public void setToPhoneNumber(String ToPhoneNumber) {
this.ToPhoneNumber = ToPhoneNumber;
}
public String getResponseReceiveDate() {
return ResponseReceiveDate;
}
public void setResponseReceiveDate(String ResponseReceiveDate) {
this.ResponseReceiveDate = ResponseReceiveDate;
}
public String getMessage() {
return Message;
}
public void setMessage(String Message) {
this.Message = Message;
}
}
I've looked at ObjectMapper but I am not sure how to work this into my controller, and truth be told I'd prefer not to have to write a bunch of extra classes if Spring will do it for free.
Simply annotate your field, getter, or setter with #JsonProperty, specifying the exact name that will appear in the JSON. For example
#JsonProperty("FromPhoneNumber")
private String FromPhoneNumber;

Categories