I can't get Spring Data REST to accept a JSON-PATCH content body, what am I doing wrong?
Simple domain:
#Document
public class DomainA {
#Id private String id;
private String atext;
public String getAtext(){ return atext;}
public void setAtext(String str){ atext = str;}
}
Backed by a MondoDB repo:
#RepositoryRestResource
public interface DomainARepository extends MongoRepository<DomainA, String> {}
I want to JSON-PATCH this,like so:
curl -X PATCH -H "Authorization: Basic dXNlcjpQVw=="
-H "Content-Type: application/json-patch+json"
-H "Cache-Control: no-cache"
-d '[{"op":"replace", "path":"/atext", "value":"JSON-PATCHed text"}]'
http://localhost:8080/domainAs/550e169209a5cc0df82c95d4
But when I do I get an exception:
org.springframework.http.converter.HttpMessageNotReadableException:
Could not read an object of type class patchex.DomainA from the request!;
nested exception is
org.springframework.http.converter.HttpMessageNotReadableException:
Could not read payload!; nested exception is java.lang.ClassCastException:
com.fasterxml.jackson.databind.node.ArrayNode cannot be cast to
com.fasterxml.jackson.databind.node.ObjectNode at
org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:183)
....
If I supply the patch body without the array brackets, I get back a 204; no exception but no change to the document, either. I can "patch" the document using PUT or PATCH of {"atext":"PUTed value"}; POSTs/GETs work fine; and, the tests in Spring Data REST seem to verify that JSON-PATCH should work if I can get the content body accepted. So, what's the right way to using PATCH here?
(Using Spring Boot 1.2.2.RELEASE starters for -data-mongodb, -data-rest, java 1.8)
EDIT: Same exception if I back with a JPA (H2) CRUD #RepositoryRestResource repo.
EDIT: Opened JIRA at https://jira.spring.io/browse/DATAREST-498
Related
I'm trying to pass a url in to a Spring MVC controller method in a #PathVariable but am getting a 400 http response code, and the request is rejected before it reaches the controller method.
My request is being issued as:
curl 'https://127.0.0.1:8443//myapi/page/http%3A%2F%2Fwww.example.co.uk%2Ffolder%2Fpage_n0/info' -k -w "\nResponse code: %{http_code}\n"
The controller method, and UTF-8 filter, is:
#Bean
public FilterRegistrationBean filterRegistrationBean()
{
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
#RequestMapping(value = {"/myapi/page/{url}/info"}, method = RequestMethod.GET)
public ResponseEntity<?> test(HttpServletRequest request, HttpServletResponse response, #PathVariable("url") String webPage)
{
ResponseEntity<?> results = new ResponseEntity<Map<String, Object>>(HttpStatus.OK);
// Do something here
return results;
}
I am using Tomcat 8.5.5, and nothing is present in the log files (logging has been set at DEBUG level) except for the following entry in localhost_access_log.2016-10-04.txt:
127.0.0.1 - - [04/Oct/2016:09:04:24 +0100] "GET //myapi/page/http%/info HTTP/1.1" 400 -
In my test code, the url being passed into is being encoded using URLEncoder.encode(), so should be being encoded correctly.
In the remote debugger, I can see that the CharacterEncodingFilter registration code above is being entered so the filter should be being registered.
I have also addedURIEncoding="UTF-8" to each of the Connectors in the server.xml file in $CATALINA_HOME/conf.
I am going round in circles with this and keep on thinking there's something obvious I'm missing. I've never had any issues using a #PathVariable before so I presume I'm encountering some sort of encoding issue, probably relating to the % sign.
I'd be grateful for any help with this!
Update:
I think the issue is that Spring, inside AbstractHandlerMethodMapping and UriUtils, is decoding the entire url, including the #PathVariable portion. It then cannot find a request mapping for the decoded url which is unsurprising since the decoded url includes the decoded url in the #PathVariable. I need to find some way of telling Spring not to decode the application/x-www.form-urlencoded portion of the url.
Any ideas?
I have a spring-boot 1.1.7 application that uses Thymeleaf for much of the UI, so the response from my controllers hasn't really been a concern. However, now I need to provide a XML response when a user submits a request via URL.
Here is a typical Request:
http://localhost:9001/remote/search?sdnName=Victoria&address=123 Maple Ave
Here is most of my gradle configuration:
project.ext {
springBootVersion = '1.1.7.RELEASE'
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion")
compile("org.springframework.security:spring-security-web:4.0.0.M1")
compile("org.springframework.security:spring-security-config:4.0.0.M1")
compile('org.thymeleaf.extras:thymeleaf-extras-springsecurity3:2.1.1.RELEASE')
compile("org.springframework.boot:spring-boot-starter-actuator")
compile('com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.5.0')
}
And here is my controller:
#Controller
public class RemoteSearchController {
#Autowired
private SdnSearchService sdnSearchService;
#RequestMapping(value = "/remote/search", method = RequestMethod.GET, produces = MediaType.APPLICATION_XML_VALUE)
public List<Sdn> search(#ModelAttribute SdnSearch sdnSearch) {
List<Sdn> foundSdns = sdnSearchService.find( sdnSearch );
return foundSdns;
}
Here is my Object to be returned:
#Entity
public class Sdn {
#Id
private long entNum;
private String sdnName;
...
//getters & setters here
}
I am able to receive the request via REST client (such as CocoaREST) and handle it. But When I return the list of SDN i get the following exception, even though I do have Jackson & jackson-dataformat-xml on my classpath:
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:229)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:301)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:248)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:57)
at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:299)
My REST Client is including a Accept Header of "text/xml" (but in all honesty I would rather them not have to set this. Ideally any call to this Controller would always get XML, regardless of header being present).
Is there a way to handle this? I thought the Media Converters were included and just returned whatever the controller told them to?
SOLUTION:
See below for the answer I posted.
I had the exact same problem and I found the solution on Spring documentation website : here
In synthesis, I added the following dependency to the pom.xml of my project :
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
Then I added the following code block to the class that the service had to return :
import javax.xml.bind.annotation.XmlRootElement;
#XmlRootElement
public class Greeting {...}
And it worked.
SOLUTION: I used a combination of both answers below (thank you very much!). I am posting here in case anyone else needs help.
My modified controller:
#Controller
public class RemoteSearchController {
#Autowired
private SdnSearchService sdnSearchService;
#RequestMapping(value = "/remote/search", method = RequestMethod.GET, produces = { "application/xml", "text/xml" }, consumes = MediaType.ALL_VALUE )
#ResponseBody
public SdnSearchResults search(#ModelAttribute SdnSearch sdnSearch) {
List<Sdn> foundSdns = sdnSearchService.find( sdnSearch );
SdnSearchResults results = new SdnSearchResults();
results.setSdns( foundSdns );
return results;
}
}
And on my client, I set the request headers:
Content-type: application/text
Accept: text/xml
I think ultimately the problem was that my client headers were not being set correctly, so I may not have had to make some of these changes. But I liked the idea of a SearchResults class containing a list of results:
#XmlRootElement
public class SdnSearchResults {
private List<Sdn> sdns;
...
}
It may be better to create a new class:
public class SdnSearchResult {
private List<Sdn> sdns;
...
}
Then, a slight change will be required to the existing classes as follows:
public interface SdnSearchService {
SdnSearchResult find(SdnSearch sdnSearch);
}
#Controller
public class UISearchController {
#Autowired
private SdnSearchService sdnSearchService;
#RequestMapping("/search")
public ModelAndView search(#ModelAttribute SdnSearch sdnSearch) {
return new ModelAndView("pages/search/results", "sdns", sdnSearchService.find(sdnSearch).getSdns());
}
}
Once this is done, the other controller must be coded as:
#Controller
public class RemoteSearchController {
#Autowired
private SdnSearchService sdnSearchService;
#RequestMapping("/remote/search")
#ResponseBody
public SdnSearchResult search(#RequestBody SdnSearch sdnSearch) {
return sdnSearchService.find(sdnSearch);
}
}
A quick explanation of the changes from your code:
#RequestBody will automatically deserialize the entire HTTP request body to an SdnSearch instance. External applications will typically submit the request data as HTTP body, so #RequestBody will ensure that the deserialization to Java object happens automatically.
#ResponseBody will automatically serialize the return value according to the external client's capabilities and the libraries available on the classpath. If Jackson is available on the classpath and the client has indicated that they can accept JSON, the return value will be automatically sent as JSON. If the JRE is 1.7 or higher (which means that JAXB is included with the JRE) and the client has indicated that they can accept XML, the return value will be automatically sent as XML.
List<Sdn> needs to be changed to SdnSearchResult to ensure that the application can exchange JSON, XML, RSS and ATOM formats with a single controller method, since XML (and XML based formats) require a root-tag on the output, which a List<Sdn> cannot be translated to.
Once these changes are done, fire up a REST client such as the Postman extension for Chrome and submit a request to /remote/search with the following information:
Request header Accepts set to application/json.
Request header Content-Type set to application/json.
Request body set to the JSON string { "sdnName" : "Victoria", "address" : "123 Maple Ave" }.
This will give you a JSON response.
You've marked the controller method as producing application/xml responses (produces = MediaType.APPLICATION_XML_VALUE). The request's accept header (Accept: text/xml) doesn't match so Spring determines that your search method cannot handle the request.
There are a few different ways to fix this on the server, depending on your exact requirements:
You could remove the produces attribute entirely
You could specify multiple media types: produces = { "application/xml", "text/xml" }
I am not sure about your version of Spring Boot (1.1.7.RELEASE) but I am on version 1.5.2.RELEASE and this xml conversion / serialization happens automatically without usage of any jackson dependencies as mentioned in few of the answers.
I guess that is happening because org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter is automatically configured since Spring Boot version 1.5.1.RELEASE & that converter uses default JAXB implementation of JRE ( so no explicit xml conversion dependency needed ) .
Second, Accept header set by clients in request decides which format the output is expected so a request mapping like below ( i.e. a single end point ) ,
#RequestMapping(method = RequestMethod.GET, value = "/remote/search", produces = {
MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE })
can be used to produce an xml as well as a JSON response ( if Accept header is set as text/xml or application/xml & application/json respectively.
Note 1 : javax.xml.bind.annotation.XmlRootElement needs to be specified on root class if xml response is expected for a Java class. This is mandatory.
Note 2 : Jackson for json is already included in Spring Boot so that is not to be explicitly included for json outputs
Note 3 : Accept header - Output match off happens automatically by framework & developer doesn't have to code anything specific for that.
So in my opinion, if you only add XmlRootElement to your base class & upgrade your Spring Boot version, your server side is all set. Responsibility to set correct Accept header lies with the clients.
In addition to what Michael told in his answer, I added the following dependencies as well to pom.xml
<dependency>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>woodstox-core-asl</artifactId>
<version>4.4.1</version>
</dependency>
For some reason, the jackson-dataformat-xml alone was not helping.
I also made sure that ResponseEntity is returned in the get call and removed the produces=MediaType from the RequestMapping annotation.
With these changes, I was able to get the correct data but I had to give the extension of mime type to the REST URL during get call. ie, specify explicitly like: http://localhost:8080/hello.xml or http://localhost:8080/hello.json in browser
In my case I wanted to return a formatted XML string and it was all combined into one line.
Adding produces = { "application/xml", "text/xml" } to the request mapping was enough to return the string as formatted XML (with indentation).
example:
#RequestMapping(method= RequestMethod.GET, value="/generate/{blabla}", produces = { "application/xml", "text/xml" })
public String getBlaBla(#PathVariable("param") String param) throws IOException {
}
Goodluck.
I'm trying to learn Spring MVC and i'm stuck at this program behaving weirdly can any one please help.
#Controller
public class GreetingController {
#RequestMapping(value="/greeting", consumes={"application/json", "application/xml"},produces = {"application/json","application/xml"}, headers = "Content-type=*/*")
public #ResponseBody Greeting greeting(
#RequestParam(value="name", required=false, defaultValue="I'm default") String name) {
Greeting obj = new Greeting("1",name);
Address address = new Address("CA, US");
obj.setAddress(address);
return obj;
}
}
Greeting and Address are just POJO's.
here is the output from 'curl' i.e irrespective of Content-type output is in JSON format.
curl --header "Content-type: application/xml" http://host.com:8080/javamvc/greeting
{"id":1,"content":"Hello, I'm default!","address":{"addr":"CA, US"}}
curl --header "Content-type: application/json" http://host.com:8080/javamvc/greeting
{"id":1,"content":"Hello, I'm default!","address":{"addr":"CA, US"}}
and then when i use 'RestClient' from mozilla i get output as xml always as irrespective of my Content-type=application/json or Content-type=application/xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><greeting><id>1</id><content>Hello, I'm default!</content><address><addr>CA, US</addr></address></greeting>
Can anyone please help?
In your curl requests, you are specifying the Content-Type of the request body, not the content type you are expecting in the response body.
For that, you need to specify the Accept header.
Spring is always producing application/json, because it's the first in the list of produces values.
You'll have to be more specific about what you are doing with Mozilla, but it seems like it's requesting with the Accept header being application/xml.
I wrote a spring-mvc controller method to get an array of values in the request parameter.The method looks like below
/**
Trying to get the value for request param foo which passes multiple values
**/
#RequestMapping(method=RequestMethod.GET)
public void performActionXX(HttpServletRequest request,
HttpServletResponse response,
#RequestParam("foo") String[] foo) {
......
......
}
The above method works fine when the request url is in below format
...?foo=1234&foo=0987&foo=5674.
However when the request url is in below format the server returns 400 error
...?foo[0]=1234&foo[1]=0987&foo[2]=5674
Any idea how to fix the method to cater to the second format request url?
This is not possible with #RequestParam. What you can do is implement and register your own HandlerMethodArgumentResolver to perform to resolve request parameters like
...?foo[0]=1234&foo[1]=0987&foo[2]=5674
into an array. You can always checkout the code of RequestParamMethodArgumentResolver to see how Spring does it.
Note that I recommend you change how the client creates the URL.
The server is supposed to define an API and the client is meant to follow it, that's why we have the 400 Bad Request status code.
I resolved this issue using the request.getParameterMap().Below is code.
Map<String,String> parameterMap= request.getParameterMap();
for(String key :parameterMap.keySet()){
if(key.startsWith("nameEntry")){
nameEntryLst.add(request.getParameter(key));
}
}
Basically, I have a project set up in Restlet which uses JAXRS for mapping resources to paths and uses JAXB for serializing and deserializing XML to/from Java types. I'm currently trying to send a POST request in order to test whether it works, and I'm running into a bit of trouble. Here's my resource:
#Path("stream")
public class StreamResource {
#POST
#Consumes("text/xml")
#Produces("text/xml")
public Stream save(Stream value) {
logger.debug("saving new stream...");
return (Stream)this.streamPersistence.save(value);
}
}
Here's my Stream class:
#XmlRootElement(name="stream")
#XmlType(propOrder={"id", "streamName", "title", "description", fileSystemPath"})
public class Stream {
private Long id;
private String streamName;
private String fileSystemPath;
private String title;
private String description;
// getters/setters omitted for brevity
}
And here's how I'm invoking curl:
curl -X POST -d '<stream><streamName>helloWorld.flv</streamName><title>Amazing Stuff, Dude!</title><description>This stream is awesome-cool.</description><fileSystemPath>/home/rfkrocktk/Desktop/helloWorld.flv</fileSystemPath></stream>' --header 'Content-Type:"text/xml"' http://localhost:8888/stream
Here's the error I'm getting from curl:
The given resource variant is not supported.
...and here's the error in Restlet:
15:02:25.809 [Restlet-961410881] WARN org.restlet.Component.Server - Error while parsing entity headers java.lang.IllegalArgumentException: Illegal token: "text
at org.restlet.data.MediaType.normalizeToken(MediaType.java:647)
at org.restlet.data.MediaType.normalizeType(MediaType.java:686)
at org.restlet.data.MediaType.<init>(MediaType.java:795)
at org.restlet.data.MediaType.<init>(MediaType.java:767)
at org.restlet.engine.http.header.ContentTypeReader.createContentType(ContentTypeReader.java:84)
at org.restlet.engine.http.header.ContentTypeReader.readValue(ContentTypeReader.java:112)
at org.restlet.engine.http.header.ContentType.<init>(ContentType.java:99)
at org.restlet.engine.http.header.HeaderUtils.extractEntityHeaders(HeaderUtils.java:664)
at org.restlet.engine.http.connector.Connection.createInboundEntity(Connection.java:313)
at org.restlet.engine.http.connector.ServerConnection.createRequest(ServerConnection.java:136)
at org.restlet.engine.http.connector.ServerConnection.readMessage(ServerConnection.java:229)
at org.restlet.engine.http.connector.Connection.readMessages(Connection.java:673)
at org.restlet.engine.http.connector.Controller$2.run(Controller.java:95)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
at java.lang.Thread.run(Thread.java:679)
What am I doing wrong here? This seems pretty straightforward, right?
Remove the quotes around text/xml.
In other words you want
curl -X POST -d '<stream><streamName>helloWorld.flv</streamName><title>Amazing Stuff, Dude!</title><description>This stream is awesome-cool.</description><fileSystemPath>/home/rfkrocktk/Desktop/helloWorld.flv</fileSystemPath></stream>' --header 'Content-Type: text/xml' http://localhost:8888/stream
curl -v -H "Content-Type: application/xml" -X POST --data-binary "#token.xml" URL