Spring #RequestMapping with optional parameters when using custom request object - java

Hello I want to make an optional parameter on my controller, id that so far looks like this
#ApiOperation(value = "get runs by date in order to paginate")
#ApiResponses({ #ApiResponse(code = 200, message = "Success"),
#ApiResponse(code = 500, message = "Unexpected failure") })
#RequestMapping(value = "/time/{date}/to/{toPage}/from/{fromPage}",
params = {"pageSize", "id"},
method = RequestMethod.GET)
public RunCollection getPolicyByDate(GetPolicyByDateRequest request) {
return serviceImpl.getListOfRunsByDate(request);
}
But this means the parameter is required. I want it to be optional. I saw this answer where they use #RequestParam Spring #RequestMapping with optional parameters but I want to include it in my GetPolicyByDateRequest object. Is this possible? The spring docs don't allude to turning off the required attribute. Could I possibly use Optional

2 options :
If your GetPolicyByDateRequest has not primitive attribute, they should be inserted by Spring in your object. If your parameter is not set (i.e there is no pageSize in your url) then Spring injects NULL in your attribute.
public class GetPolicyByDateRequest {
private String date;
private int toPage;
private int fromPage;
private Integer pageSize;
private Integer id;
public GetPolicyByDateRequest() {
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public int getToPage() {
return toPage;
}
public void setToPage(int toPage) {
this.toPage = toPage;
}
public int getFromPage() {
return fromPage;
}
public void setFromPage(int fromPage) {
this.fromPage = fromPage;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Override
public String toString() {
return "MyRequest{" +
"date='" + date + '\'' +
", toPage=" + toPage +
", fromPage=" + fromPage +
", pageSize=" + pageSize +
", id=" + id +
'}';
}
}
Weirdly, I think it is some king of bug in Spring, even when using Optional you get a null object if the parameter in the requestParam is not provided.
If you want to get all parameters, you need to use #PathVariable with #RequestParam in your case. Here is how to get your parameters :
#RequestMapping(value = "/time/{date}/to/{toPage}/from/{fromPage}", method = RequestMethod.GET)
public RunCollection getPolicyByDate(#PathVariable String date, #PathVariable int toPage, #PathVariable int fromPage, #RequestParam(required = false) Optional<Integer> pageSize, #RequestParam(required = false) Optional<Integer> id) {
// then use your parameters the way you to use then.
}
Please note :
#PathVariable retrieves the parameter in the URL, where the value is between {}, like {date} -> #PathVariable String date
#RequestParam retrieves the parameter after the ? in the URL. ?pageSize=10 -> #RequestParam int pageSize.
Both of this annotation takes as parameters, among others, required=true|false. When do mark a parameter as not required, make sure you don't use a primitive type, like int, otherwise, you can set null to this object, and your code will fail at runtime. This is the reason why I have used Optional<Integer>, to allow the type. But Spring is clever, and if it detects the Optional in your parameter, then you are forced to use required=false in the #RequestParam.
If you wanted to populate your GetPolicyByDateRequest, then you should have done a POST mapping, and use the annotation #RequestBody GetPolicyByDateRequest request, and send a JSON body in your request to make Spring (actually Jackson) translate your JSON into a GetPolicyByDateRequest object.
Hope it helps

Related

How to pass not null values #RequestParameter in controller?

I am trying to update an Entity by using spring boot 2.5.3 in the controller method.
http://localhost:5000/api/v1/student/1
with the following payload.
{
"name":"abc",
"email":"abc#email.com",
"dob":"2000-06-14"
}
These values are not updated. They are getting null values when I inspected them using a debugger.
Here is my controller method.
#PutMapping(path = "/{id}")
public ResponseEntity<?> updateStudent(#PathVariable("id") Long id, #RequestParam(required = false) String name, #RequestParam(required = false) String email) {
Student savedStudent = studentService.updateStudent(id, name, email);
return ResponseEntity.ok(savedStudent);
}
Email and name are optional.
In debugger: name:null,email:null. Why are they getting null values?
What is the correct way to pass values from the controller?
#Transactional
// We are not using any query from the repository because we have the service method with transactional annotation.
public Student updateStudent(Long studentId, String name, String email) {
Student student = studentRepository.findById(studentId).orElseThrow(()->new EntityNotFoundException("Student with id " + studentId + " does not exists."));
if (name!= null && name.length()>0 && !Objects.equals(name,student.getName())){
student.setName(name);
}
if (email!= null && email.length()>0 && !Objects.equals(email,student.getEmail())){
Optional<Student> optionalStudent = studentRepository.findStudentByEmail(email);
if (optionalStudent.isPresent()){
throw new IllegalStateException("Email is already taken");
}
student.setEmail(email);
}
System.out.println(student);
Student savedStudent= studentRepository.save(student);
return savedStudent;
}
{
"name":"abc",
"email":"abc#email.com",
"dob":"2000-06-14"
}
This is not a request parameter but the request body. You need to create a class and use #RequestBody annotation.
#Data
public class UpdateStudentRequest {
private String id;
private String name;
private String email;
}
#PutMapping(path = "/{id}")
public ResponseEntity<?> updateStudent(#PathVariable("id") Long id, #RequestBody UpdateStudentRequest request) {
Student savedStudent = studentService.updateStudent(
request.getId(), request.getName(), request.getEmail());
return ResponseEntity.ok(savedStudent);
}
If you want to send the request parameters as... URL parameters:
http://localhost:5000/api/v1/student/1?name=abc&email=abc#email.com
You aren't sending it as a param (after ?).
http://localhost:5000/api/v1/student/1?name=John Could do the trick.
Since you are POSTing an HTTP request with a content body (being in JSON in your case), you need to map the body using the #RequestBody annotation:
#PutMapping(path = "/{id}")
public ResponseEntity<?> updateStudent(#PathVariable("id") Long id, #RequestBody StudentDTO student) {
Student savedStudent = studentService.updateStudent(
id, student.getName(), student.getEmail());
return ResponseEntity.ok(savedStudent);
}
The StudentDTO would be a lightweight type reflecting your input payload:
public class StudentDTO {
private String name;
private String email;
private String dob;
// setters and getters
}
Otherwise, to keep your RestController signature and use the #RequestParametrized fields, you should send a request of following shape:
http://localhost:5000/api/v1/student/1?name=abc&email=abc#email.com&dob=2000-06-14

boolean is set to false if it's not present in #RequestBody

I've stumbled upon interesting case and I'm not sure how to resolve it. It's probably related to JSON Post request for boolean field sends false by default but advices from that article didn't help.
Let's say I have this class:
public class ReqBody {
#NotNull
#Pattern(regexp = "^[0-9]{10}$")
private String phone;
//other fields
#NotNull
#JsonProperty(value = "create_anonymous_account")
private Boolean createAnonymousAccount = null;
//constructors, getters and setters
public Boolean getCreateAnonymousAccount() {
return createAnonymousAccount;
}
public void setCreateAnonymousAccount(Boolean createAnonymousAccount) {
this.createAnonymousAccount = createAnonymousAccount;
}
}
I also have endpoint:
#PostMapping(value = "/test", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MyOutput> test(
#ApiParam(value = "information", required = true) #RequestBody ReqBody input
) {
//do something
}
problem is when I send my request body as:
{
"phone": "0000000006",
"create_anonymous_account": null
}
or just like
{
"phone": "0000000006"
}
it sets createAnonymousAccount to false.
I have checked, and it correctly recognises "create_anonymous_account": true
Is there any way to "force" null value in boolean field?
I really need to know if it was sent or no, and not to have default value.
You can use Jackson annotation to ignore the null fields. If the Caller doesn't send createAnonymousAccount then it will be null.
#JsonInclude(JsonInclude.Include.NON_NULL)
public class ReqBody {
#NotNull
#Pattern(regexp = "^[0-9]{10}$")
private String phone;
//other fields
#JsonProperty(value = "create_anonymous_account")
private Boolean createAnonymousAccount ;
}

How to add multiple RestController endpoints differed by query-params?

How can I simply redirect a url if a specific query parameter is missing?
#RestController
public class PersonController {
//only in case the "sort" query parameter is missing
#GetMapping("/persons")
public String unsorted() {
return "redirect:/persons?sort=name";
}
//only in case the "sort" query parameter exists
#GetMapping("/persons")
public String sorted() {
//...
}
}
Use #RequestParam to extract query parameters
Add parameter for #RequestParam: value, defaultValue, required
with java >= 8:
#RestController
public class PersonController {
#GetMapping("/persons")
public String personList(#RequestParam(value = "sort", defaultValue = "name") Optional<String> sort) {
//handling process here
}
}
with java < 8:
#RestController
public class PersonController {
#GetMapping("/persons")
public String personList(#RequestParam(value = "sort", defaultValue = "name", required=false) String sort) {
//handling process here
}
}
You could use #GetMapping.params
#GetMapping(value = "/persons", params = "sort")
public String sorted() {
You can use the params element. One mapping will supports params="sort" for when the sort parameter is present and the other params="!sort" for when it is missing.
However, you may want to consider using a default value instead of performing a redirect. What benefit does the redirect provide? It will require the server respond and then and have the client make a second HTTP request.
Using params
#RestController
public class PersonController {
//only in case the "sort" query parameter is missing
#GetMapping(value = "/persons", params = "!sort")
public String unsorted() {
return "redirect:/persons?sort=name";
}
//only in case the "sort" query parameter exists
#GetMapping(value = "/persons", params = "sort")
public String sorted() {
//...
}
}
Using default value
#RestController
public class PersonController {
//only in case the "sort" query parameter exists
#GetMapping("/persons")
public String sorted(
#RequestParam(name = "sort", defaultValue = "name") String sort)
{
//...
}
}
You can set a default value:
#RestController
public class PersonController {
//only in case the "sort" query parameter is missing
#GetMapping("/persons")
public String unsorted(#RequestParam(value = "sort", defaultValue = "name") String name) {
// do logic
}
}
You can also set the default value for the missing value and continue forward
#RequestParam(value = "sort", defaultValue = "name") String name

Spring Rest Controller PUT method request body validation?

Here is what i have in my controller.
#RequestMapping(value = "/accountholders/{cardHolderId}/cards/{cardId}", produces = "application/json; charset=utf-8", consumes = "application/json", method = RequestMethod.PUT)
#ResponseBody
public CardVO putCard(#PathVariable("cardHolderId") final String cardHolderId,
#PathVariable("cardId") final String cardId, #RequestBody final RequestVO requestVO) {
if (!Pattern.matches("\\d+", cardHolderId) || !Pattern.matches("\\d+", cardId)) {
throw new InvalidDataFormatException();
}
final String requestTimeStamp = DateUtil.getUTCDate();
iCardService.updateCardInfo(cardId, requestVO.isActive());
final CardVO jsonObj = iCardService.getCardHolderCardInfo(cardHolderId, cardId, requestTimeStamp);
return jsonObj;
}
This is the request body bean:-
public class RequestVO {
private boolean active;
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
The issue that I am having is when i sent the request body as
{"acttttt":true} the active is set to false it updates the cardinfo with false. Whatever wrong key value i sent the active is considered as false. How would I handle this is their a way. Every other scenario is handled by spring with a 404.
Any help or suggestion is appreciated.
Because the default value for primitive boolean is false. Use its corresponding Wrapper, Boolean, instead:
public class RequestVO {
private Boolean active;
// getters and setters
}
If the active value is required, you can also add validation annotations like NotNull:
public class RequestVO {
#NotNull
private Boolean active;
// getters and setters
}
Then use Valid annotation paired with RequestBody annotation to trigger automatic validation process.

java service 400 bad request error

Dear spring Java professionals
please help me out in this :
I have a custom service in spring and I dont have any errors on my wildfly server when i run it . but when I do the below update request i am getting 400 bad request though im sending the format as specified in my controller
inside my controller :
#RequestMapping(value = "/user/updatefilters/{Id}", method = RequestMethod.POST)
public Response updateFilter(#PathVariable("Id") Long Id, #RequestBody #Valid Filter Filter) {
FilterService.updateFilter(Id, Filter);
HashMap<String, Object> response = new HashMap<>();
response.put("messages", null);
response.put("success", Boolean.valueOf(true));
return Response.instance().friendlyName("filter-updated").object(response).statusCode(HttpStatus.OK);
}
inside my service file :
public void updateFilter(Long Id,Filter Filter) {
List<Filter> currentFilter = FilterRepo.getFilters(Id, Filter.getFilterId().longValue(),null);
currentFilter.get(0).setLabel(Filter.getLabel());
FilterRepo.save(currentFilter.get(0));
for (FilterField FilterField : Filter.getFilterFields()) {
FilterField currentFilterField = FilterFieldRepo.getFilterField(FilterField.getfieldId());
if (currentFilterField != null) {
currentFilterField.setfield(FilterField.getfield());
currentFilterField.setTypeId(FilterField.getTypeId());
FilterFieldRepo.save(currentFilterField);
}
}
}
inside my repository :
public List<Filter> getFilterList(Long Id, String type) {
List<Filter> FilterField = FilterFieldRepo.getFilterFields(Id,type);
return FilterField;
}
public void updateFilter(Long Id,Filter Filter) {
List<Filter> currentFilter = FilterRepo.getFilters(Id, Filter.getFilterId().longValue(),null);
currentFilter.get(0).setLabel(Filter.getLabel());
FilterRepo.save(currentFilter.get(0));
for (FilterField FilterField : Filter.getFilterFields()) {
FilterField currentFilterField = FilterFieldRepo.getFilterField(FilterField.getfieldId());
if (currentFilterField != null) {
currentFilterField.setfield(FilterField.getfield());
currentFilterField.setTypeId(FilterField.getTypeId());
FilterFieldRepo.save(currentFilterField);
}
}
}
Please note that inside my entity I added a transient list like this :
#Transient
private List<FilterField> filterFields;
updated :
this is my Filter class i generated the crud in netbeans but added the transuent list manually:
#Entity
#Table(schema="hitmeister",name = "filters")
#NamedQueries({
#NamedQuery(name = "Filter.findAll", query = "SELECT s FROM Filter s"),
#NamedQuery(name = "Filter.findByFilterId", query = "SELECT s FROM Filter s WHERE s.filterId = :filterId"),
#NamedQuery(name = "Filter.findById", query = "SELECT s FROM Filter s WHERE s.Id = :Id"),
#NamedQuery(name = "Filter.findByLabel", query = "SELECT s FROM Filter s WHERE s.label = :label"),
#NamedQuery(name = "Filter.findByInsertionDate", query = "SELECT s FROM Filter s WHERE s.insertionDate = :insertionDate"),
#NamedQuery(name = "Filter.findByIsActive", query = "SELECT s FROM Filter s WHERE s.isActive = :isActive"),
#NamedQuery(name = "Filter.findByType", query = "SELECT s FROM Filter s WHERE s.type = :type")})
public class Filter implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "filter_id")
private Integer filterId;
#Basic(optional = false)
#NotNull
#Column(name = "id")
private int Id;
#Basic(optional = false)
#NotNull
#Size(min = 1, max = 500)
#Column(name = "label")
private String label;
#Basic(optional = true)
#Column(name = "insertion_date")
#Temporal(TemporalType.TIMESTAMP)
private Date insertionDate;
#Column(name = "is_active")
private Boolean isActive;
#Basic(optional = false)
#NotNull
#Size(min = 1, max = 20)
#Column(name = "type")
private String type;
#Transient
private List<FilterField> filterFields;
public Filter() {
}
public Filter(Integer filterId) {
this.filterId = filterId;
}
public Filter(Integer filterId, int Id, String label, Date insertionDate, String type) {
this.filterId = filterId;
this.Id = Id;
this.label = label;
this.insertionDate = insertionDate;
this.type = type;
}
public Integer getFilterId() {
return filterId;
}
public void setFilterId(Integer filterId) {
this.filterId = filterId;
}
public int getId() {
return Id;
}
public void setuserId(int Id) {
this.userId = userId;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Date getInsertionDate() {
return insertionDate;
}
public void setInsertionDate(Date insertionDate) {
this.insertionDate = insertionDate;
}
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
#Override
public int hashCode() {
int hash = 0;
hash += (filterId != null ? filterId.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Filter)) {
return false;
}
Filter other = (Filter) object;
if ((this.filterId == null && other.filterId != null) || (this.filterId != null && !this.filterId.equals(other.filterId))) {
return false;
}
return true;
}
#Override
public String toString() {
return " Filter #"+ filterId ;
}
public List<FilterField> getFilterFields() {
return filterFields;
}
public void setFilterFields(List<FilterField> filterFields) {
this.filterFields = filterFields;
}
}
If you need my entity code i can post it as well
Thanks In advance !
My first recommendation: (OP tried and it didn't work, she was sending POST request)
Change your mapping as below and I think you should be fine. Request from browser address bar is a GET request.
As you can see below, HTTP 400 comes when server is unable to understand the request client is sending, and in your case you are sending GET but server has nothing for GET but for POST, so 400.
W3C HTTP 400
10.4.1 400 Bad Request
The request could not be understood by the server due to malformed
syntax. The client SHOULD NOT repeat the request without
modifications.
Code fix:
#RequestMapping(value = "/user/updatefilters/{Id}", method = RequestMethod.GET)
My second recommendation:
I am not Spring expert but here are my 2 cents you can try based on the JSON object you have provided and your Filter mapping - (1.) Change userId to Id, (2.) Have insertionDate as NULL, instead of an empty string.
Make sure your JSON string variables are mapped case-sensitively with your Filter class mapping, and their values are compatible with reference types.
Either your request format is not what Spring expects, or one of the Filter validations is failing. Add a org.springframework.validation.Errors argument and dump the values to find out what validations failed.
public Response updateFilter(#PathVariable("Id") Long Id, #RequestBody #Valid Filter Filter, Errors filterErrors) {
You can sniff the actual traffic using curl or a network monitoring tool to make sure the HTTP transaction is really what you think it is.
EDIT: Having looked at the JSON in one of your comments, I think this is going to turn out to be upper/lower case in your JSON field names. Either change "Id" to "id" and "FilterId" to "filterId", or annotate the Filter fields with #XmlElement(name = "Id") and #XmlElement(name = "FilterId"). Java Bean property names are case sensitive.
EDIT 2: Filter.setuserId(int Id) is broken as well. You need a setId() method for deserializing the bean, and you need to change the method so it stores the passed argument instead of just setting userId to itself.

Categories

Resources