I'm trying to build a very basic REST API using Spring.
My URL endpoints are:
GET /notes
GET /notes/{noteId}
POST /notes
PUT /notes/{noteId}
DELETE /notes/{noteId}
All these endpoints work perfectly fine as expected except the PUT request which I want to run to update an item.
The problem is that data is not being received via PUT, where as it works fine for POST.
Here's my controller; I've tested it by adding a method identical to the update method but using POST and that works fine. I don't know why?
package notes;
import org.springframework.web.bind.annotation.*;
#RestController
#RequestMapping("/notes")
public class NotesController {
...
#RequestMapping(value="/{noteId}", method=RequestMethod.PUT)
public Response update(#PathVariable Integer noteId, Note note) {
return new Response("Note Updated", note);
}
#RequestMapping(value="/{noteId}", method=RequestMethod.POST)
public Response updateWithPost(#PathVariable Integer noteId, Note note) {
return new Response("Note Updated", note);
}
...
}
Using postman, I've tested POST http://127.0.0.1:8080/notes/5 and the response was:
{
"message": "Note Updated",
"note": {
"id": null,
"title": "Hello World detail content",
"text": "Hello World",
"createdAt": "72, Mar 2015",
"updatedAt": "72, Mar 2015"
}
}
But for PUT http://127.0.0.1:8080/notes/5 with exactly same data the response was:
{
"message": "Note Updated",
"note": {
"id": null,
"title": "",
"text": "",
"createdAt": "72, Mar 2015",
"updatedAt": "72, Mar 2015"
}
}
Update Request
For both PUT & POST request I'm sending the same test data:
title: Hello World
text: Hello World detail content
Response Class
package notes;
public class Response {
private String message;
private Note note;
public Response(String text) {
setMessage(text);
}
public Response(String text, Note note) {
setMessage(text);
setNote(note);
}
//...getters/setters
}
Note Class
package notes;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Note {
private Integer id = null;
private String title = "";
private String text = "";
private Date createdAt = new Date();
private Date updatedAt = new Date();
public Note() {}
public Note(String title, String text) {
this.setTitle(title);
this.setText(text);
}
//...getters/setters
}
ApplicationConfig
package notes;
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
#EnableWebMvc
#Configuration
public class ApplicationConfig {
}
I don't know why this is not working?
This is a limitation of the Servlet Spec and the inner workings of Spring for populating model attributes.
First, the spec says
3.1.1 When Parameters Are Available
The following are the conditions that must be met before post form data will be populated to the
parameter set:
The request is an HTTP or HTTPS request.
The HTTP method is POST.
The content type is application/x-www-form-urlencoded.
The servlet has made an initial call of any of the getParameter family of methods on the request object. If the conditions are not met
and the post form data is not included in the parameter set, the post
data must still be available to the servlet via the request object's
input stream. If the conditions are met, post form data will no longer
be available for reading directly from the request object's input
stream.
Second, your handler methods' second parameter, the one of type Note, is actually considered a model attribute, as if it was implicitly annotated with #ModelAttribute. As such, it is processed by Spring's ModelAttributeMethodProcessor. This HandlerMethodArgumentResolver uses the getParameter (and its family of methods) for populating the created instance's fields.
Since this is a PUT request, the parameters are not retrievable through getParameter. However, they are still accessible through the request body, but Spring doesn't go there for model attributes.
You can do the conversion yourself. But I suggest you change your request content to JSON or XML for PUT and use #RequestBody annotated parameters.
There is a long standing request to add parameters for PUT requests.
Don't know if it's the case here (or if it is even relevant nowadays, haven't worked with REST-stuff for a while), but I've hit a problem in the past where Jetty and Tomcat handled PUT-requests differently (the application itself was using Spring MVC similarly as your code), the other expected the parameters to be in the URL and the other wanted them in the body, like with POST.
Apparently the HTTP-spec isn't very precise on this, so different containers can handle it differently. More information here.
Related
I am working on a Spring boot project, it produces strange behaviors, for ex:
I have two APIs as follow
Controller file
#GetMapping("/list/employees")
public ResponseEntity<List<Employee>> getEmployees(){
List<Employee> list = employeeService.getAllEmployees();
return new ResponseEntity<List<Employee>>(list, new HttpHeaders(), HttpStatus.OK );
}
#GetMapping("employee/{id}")
public ResponseEntity<Employee> getEmployeeById(#PathVariable("id") long id) throws RuntimeException{
Employee employee = employeeService.getEmployee(id);
return new ResponseEntity<Employee>(employee,new HttpHeaders(),HttpStatus.OK);
}
Service file
/* return all employees */
public List<Employee> getAllEmployees(){
List<Employee> listEmployee = employeeRepo.findAll();
if(listEmployee.size()>0){
return listEmployee;
}else{
return new ArrayList<Employee>();
}
}
/*
RETURN SINGLE EMPLOYEE BY ID
*/
public Employee getEmployee(long id) throws RuntimeException{
Optional<Employee> employee = employeeRepo.findById(id);
if(employee.isPresent()){
return employee.get();
}else{
new RuntimeException("Record not found");
}
return null;
}
But running them in Postman gives weird output, for ex:
Correct behavior of second API returning single employee
http://127.0.0.1:8080/employee/3
{
"id": 3,
"firstName": "Caption",
"lastName": "America",
"email": "cap#marvel.com"
}
Incorrect behavior of the same API (I am typing the wrong path this time)
http://127.0.0.1:8080/employees/3
The API path is wrong (employees/3)
{
"firstName": "Caption",
"lastName": "America",
"email": "cap#marvel.com",
"_links": {
"self": {
"href": "http://127.0.0.1:8080/employees/3"
},
"employee": {
"href": "http://127.0.0.1:8080/employees/3"
}
}
}
same behavior with the root URI, I have not triggered any action with home URI but still gives output like in the above API.
what can be the reason for these unwanted API calls?
Looks like you have Spring Data Rest on your class path. It will automatically wire paths based on the repositories. That second response is a HATEOAS response.
A simple test would be to check maven/gradle. If you see spring-data-rest, comment it out and try again.
There is no unwanted API calls. That is the way HATEOS response is represented as stated in the documentation:
The fundamental idea of hypermedia is to enrich the representation of a resource with hypermedia elements. The simplest form of that are links. They indicate a client that it can navigate to a certain resource. The semantics of a related resource are defined in a so-called link relation.
As suggested above, try to look for spring boot hateos dependency and comment or remove that, then it should revert back to normal REST JSON response.
If you are using maven, look for :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
if you are using gradle, look for :
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
I am working on an app written in java and angular. The request goes from angular with params and I need to read the params in the rest services. The angular piece is fixed (I cannot change it), but I can update the java code as needed.
My issue: I cannot read the params in my java services method. The value is always null.
angular code:
alert("params : "+JSON.stringify(params, null, 2));
return $http.get(url, {
params: params
}).success(function (data) {
// success criteria
});
The params looks like this based on the alert statement:
{
"start": 0,
"end": 100,
"type": "asdf",
"name": null,
"values": [],
"locs": [],
"test1": "Val"
}
My java code looks like this:
#GET
#Path("valuesList")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.TEXT_XML)
public String getValues(#RequestParam("type")String type) throws Exception {
System.out.println("type ================"+type);
}
The type is null. I want to read all the values, but for now I am just using the type for testing. I also tried various #Consumes values, but nothing helps.
I think I need to change the #RequestParam, but not sure to what though. Thanks for any pointers.
Just wanted to post the solution - I had to update the #RequestParam to #QueryParam. The updated method call is below
public String getValues(#QueryParam("type")String type) throws Exception {
System.out.println("type ================"+type);
}
My problem is similar to the following posts:
JSON ajax POST to Spring Portlet Controller #ResourceMapping conversion issue and #ResourceMapping that accepts JSON from Ajax request
I have tried the Tipps there, but without success.
I have the following Technologies in place:
liferay-portal 6.2 CE
custom portlet-plugin for liferay based on spring 3.0.7
kendo-ui for jsp
On the client-side I produce a stringified json-Object with the functionality of kendo-ui for jsp, which is submitted in the request body. Currently it contains just some filter-parameters (but it can also contain additional parameters for server-side paging, sorting, grouping,..).
In Firefox developer tools the request-body (payload) looks like following:
{
"filter" : {
"logic" : "and",
"filters" : [{
"field" : "name",
"value" : ""
}, {
"field" : "city",
"value" : ""
}, {
"field" : "zip",
"value" : ""
}, {
"field" : "country",
"value" : ""
}
]
}
}
On the server-side I have a POJO for that structure. I tested this in a Spring Web MVC Servlet enviroment with success. Using #RequestBody and Jackson the deserialization of the JSON Object works.
Working in liferay-portlet enviroment I cannot use #RequestBody and httpServletRequest.
The Controller looks like following:
#ResourceMapping(value = "test")
public void searchProviderTest(ResourceRequest request, ResourceResponse response,
#ModelAttribute("filter") DataSourceRequest dataSourceRequest) {
LOGGER.info(">>>>>> JsonOjekt per Parameter übergeben: " + request.getParameter("filter"));
LOGGER.info(">>>>>>>> DatasourceRequest: " + dataSourceRequest);
}
The DataRequestObject has no values. I see all the attributes, but they are empty. And there is no request parameter "filter" (as exspected)
Here is my DataSourceRequest-Object (abstract):
public class DataSourceRequest {
private int page;
private int pageSize;
private int take;
private int skip;
private List<SortDescriptor> sort;
private List<GroupDescriptor> group;
private List<AggregateDescriptor> aggregate;
private HashMap<String, Object> data;
private FilterDescriptor filter;
public DataSourceRequest() {
filter = new FilterDescriptor();
data = new HashMap<String, Object>();
}
...(getters and setters)
public static class FilterDescriptor {
private String logic;
private List<FilterDescriptor> filters;
private String field;
private Object value;
private String operator;
private boolean ignoreCase = true;
public FilterDescriptor() {
filters = new ArrayList<FilterDescriptor>();
}
...(getters and setters)
Im am searching for a solution since a few weecks, but I do not get the JSON-Object converted (deserialized?) to the DataSourceRequest-Object using the portlet-controller. I even do not have an idea how to access the JSON-String in the request-body (payload) from the portlet-controller.
Following the second mentioned post, the nested objects might be the problem. I contacted the kendo-ui support with the question, how I can submit the request to get the format described in the post. But they told me, that is not possible (e.g. using parameterMap-attribute of the datasource object)and I have to solve it on the server-side.
The first post describes a solution with #ModelAttribute, but then I get only the empty object-and when I try to get the JSON with #RequestParam I get an error, that the parameter is not in the request (I think because it is in the body)
I was thinking about setting up an additional RESTFul API, based on Spring Web MVC Servlet - I even tried it and it works- but I am not sure if that is really meaningful, because liferay already has a RESTFul -API.
Is there a way to convert the JSON Object to an JAVA Object inside the Portlet Controller ? or Do I need the additional API?
Any tips are welcome!!
I had the same problem while serializing and deserializing Json with Liferay. The solution for me was to send the json as a parameter in a form-data. That way a I was able to retrive the Json with the following method:
String paramJson = ParamUtil.getString(request, "myJson");
And then make use of Gson api to deserialize:
new Gson().fromJson(paramJson, MyPOJO.class);
You won't have so many troubles with Gson.
You can also use Gson to serialize objects in the return of your services, this will avoid problems with nested objects witch Liferay doesn't serialize properly.
This code show how to send the Json as request body:
The request will be processed by method 'serveResource' in a MVCPortlet.
var portletUrl = Liferay.PortletURL.createResourceURL();
portletUrl.setPortletId(<portletId>);
portletUrl.setResourceId('publicar'); // Any identifier
var formData = new FormData();
formData.append(<portlet-namespace> + 'myJson', JSON.stringify(object));
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', callbackSuccess, false);
xhr.open('POST', urlPortlet);
xhr.send(formData);
To share my experience hera are the steps:
in JS set contentType to application/x-www-form-urlencoded. Thats the code for kendo-ui (uses jQuery Ajax in Background)
<kendo:dataSource-transport-parameterMap>
function parameterMap(options,type) {
if(type==="read"){
return "osdeFilter=" + encodeURIComponent(JSON.stringify(options));
} else {
return "osdeModels=" + encodeURIComponent(JSON.stringify(options.models));
}
}
</kendo:dataSource-transport-parameterMap>
Get the Parameter and in my case use Jackson manualy to deserialize the JSON String
#ResourceMapping(value = "test")
public void searchProviderTest(ResourceRequest request, ResourceResponse response)
throws JsonParseException, JsonMappingException, IOException {
String osdeFilter = URLDecoder.decode(request.getParameter("osdeFilter"),"UTF-8");
LOGGER.info(">>>>>> JsonOjekt per Parameter übergeben: " + request.getParameter("osdeFilter"));
ObjectMapper objectMapper = new ObjectMapper();
DataSourceRequest dataSourceRequest = objectMapper.readValue(osdeFilter, DataSourceRequest.class);
LOGGER.info(">>>>>>>> DatasourceRequest: " + dataSourceRequest);
}
Differ from #giovani I do not need to submit the portlet-namespace. To achieve that, you must add the following configuration to liferay-portlet.xml
<requires-namespaced-parameters>false</requires-namespaced-parameters>
let's say I've got a REST API which I could get list of books by invoking following retrofit 2 request.
public interface AllRecordsFromRequestInterface {
#GET("books/all")
Call<List<TrackInfo>> operation(#Header("Authorization") String authentication_token);
}
and API response:
[
{
"id": "1",
"title": "The Catcher in the Rye",
"author":"J. D. Salinger"
},
{
"id": "2",
"title": "The Great Gatsby",
"author":"F. Scott Fitzgerald"
}
]
I use GsonConverterFactory to convert json to a Model. here is my model class
public class Book{
private int id;
private String title;
private String author;
}
I'm using a authentication token to authorize myself to API as it can be seen in my request. some times other response are received rather than above response because of token expiration or something else. for example:
{
"status": "error",
"message": "Expired token"
}
what is the proper way to handle dynamic responses (with known structure) in retrofit 2?
you have multiple choices:
1-change your API:(this one is standard)
change it like this for every response and if the user failed with authentication leave the result null or if authentication was successful put the list in the result.
{
"status" : "error/success"
"message" : ...
"result" : ....
}
2- you can give Object type to retrofit and after the response was successful you can cast it to one of your models, using "instance of" syntax.
public interface AllRecordsFromRequestInterface {
#GET("books/all")
Call<Object> operation(#Header("Authorization") String authentication_token);
}
Here is the Rest Controller Service signature
#RequestMapping(value = "/getFilteredReport", method = RequestMethod.POST)
public void getFilteredReport(#RequestBody FilteredReportVO filteredReportVO,HttpServletRequest request,HttpServletResponse response) {
Now below is the JSON structure I am sending
{
"filterAttributesFactory":{
"930000":{
"metaDataId":930000,
"displayText":"Select Category",
"attributeType":211009,
"userInputValue":null,
"dropDownoptions":null,
"isMandatory":false,
"isDropDown":false,
"defaultValue":null,
"isdefaultValue":false,
"constraintId":null
},
"930001":{
"metaDataId":930001,
"displayText":"Item Status",
"attributeType":211005,
"userInputValue":null,
"dropDownoptions":{
"157005":"FC - fake scrap",
"157006":"FH - firearm hold",
"157008":"IN - inventory"
},
"isMandatory":false,
"isDropDown":true,
"defaultValue":null,
"isdefaultValue":false,
"constraintId":213007
}
},
"reportId":132030,
"location":1202
}
Here is the FilteredReportVO POJO
public class FilteredReportVO {
private HashMap<Integer,FilterAttributeVO> filterAttributesFactory=new HashMap<Integer,FilterAttributeVO>();
private Integer reportId;
private Long location; .....GETTERS and setters below
FilterAttributeVO pojo structure is below..
public class FilterAttributeVO {
Integer metaDataId;
//String elementName;
String displayText;
Integer attributeType;
Object userInputValue;
Map<Integer,String> dropDownoptions;
Boolean isMandatory=false;;
Boolean isDropDown=false;
Object defaultValue;
Boolean isdefaultValue=false;
Integer constraintId=null;...Getters n setters..
I am hitting the service through POSTMAN plugin.
Getting error:
"The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications".
FYI in POSTMAN I am putting the JSON structure inside "Body", selected "raw", type as "JSON(application/json)".
Notice I am using 2 object type attributes userInputValue and defaultValue inside FilteredAttributeVO. R we allowed to keep object type?
Where is the problem here?
If you see the screen shot of your input JSON with JSONlint your json is not valid.Please fix your json object and validate it using jsonlint
you can try following code in your Test to Resolve this issue coz Spring internally uses Jackson library
ObjectMapper mapper = new ObjectMapper();
Staff obj = mapper.readValue(jsonInString, FilteredReportVO.class);
If this works fine then their should be issue with your Postman RequestBody preparation otherwise you will get detailed stacktrace :)
The problem was with FilteredReportVO POJO. I was setting values of the attributes "location" and "reportId" through a constructor. No setter methods were defined for these 2 attributes.
It was my bad, if I would have posted the complete POJO class u guys must have figured it out. Anyway thanks everyone for ur help