How to set priority to Spring-Boot request mapping methods - java

I have a Spring-Boot (v2.0.2) application with a RestController with 2 methods which only differ by the Accept header. A simplified version of the code is this:
#RestController
#RequestMapping("/myapp")
public class FooController {
#GetMapping(value = "/foo/{id}", headers = "Accept=application/json", produces = "application/json;charset=UTF-8")
public ResponseEntity<String> fooJson(#PathVariable id) {
return foo(pageId, true);
}
#GetMapping(value = "/foo/{id}", headers = "Accept=application/ld+json", produces = "application/ld+json;charset=UTF-8")
public ResponseEntity<String> fooJsonLd(#PathVariable id) {
return foo(pageId, false);
}
private ResponseEntity<String> foo(String id, boolean isJson) {
String result = generateBasicResponse(id);
if (isJson) {
return result
}
return addJsonLdContext(result);
}
This works fine. If we sent a request with accept header such as application/json;q=0.5,application/ld+json;q=0.6 for example it will return a json-ld response as it should.
My problem is that if we sent a request with no accept header, an empty accept header or a wildcard */* then it will by default always return a json response whereas I want the default response to be json-ld.
I've tried various things to make the json-ld request mapping take priority over the json one:
Reversing the order in which the mappings are declared.
Adding an #Order annotation to both methods (with value 1 for json-ld and value 2 for the json method)
Creating different classes and putting the #Order annotation at class-level
Adding Accept=*/* as a second accept header to the json-ld mapping does work in giving it preference but has the unwanted side-affect that all accept headers are accepted, even unsupported types as application/xml for example.
The only solution I can think of is creating one request-mapping method that accepts both headers and then processing the accept header ourselves, but I don't really like that solution. Is there a better, easier way to give preference to json-ld?

After some more searching this question on configuring custom MediaTypes pointed me in the right direction.
The WebMvcConfigurerAdapter (Spring 3 or 4) or WebMvcConfigurer (Spring 5) allows you to set a default mediatype like this:
public static final String MEDIA_TYPE_JSONLD = "application/ld+json";
#EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.valueOf(MEDIA_TYPE_JSONLD));
}
}
This works great for requests with no or an empty accept header, as well as accept: */*. However when you combine an unsupported type with the wildcard, for example accept: */*,text/plain it will return json instead of json-ld!? I suspect this is a bug in Spring.

I solved the issue using the consumes in the #GetMapping annotation. According to the official documentation:
The format is a single media type or a sequence of media types, with a request only mapped if the Content-Type matches one of these media types. Expressions can be negated by using the "!" operator, as in "!text/plain", which matches all requests with a Content-Type other than "text/plain".
In the solution bellow, note that I've added the consumes array to the normal json request mapping, making the client only be able to use the json endpoint if it have the correct Content-Type. Other requests go to the ld+json endpoint.
#GetMapping(value = "/json", headers = "Accept=application/json", consumes = {"application/json"})
#ResponseBody
public String testJson() {
return "{\"type\":\"json\"}";
}
#GetMapping(value = "/json", headers = "Accept=application/ld+json")
#ResponseBody
public String textLDJson() {
return "{\"type\":\"ld\"}";
}

Related

Find the Content-type of the incoming request in Spring boot

I have a Spring-Boot controller application that will be called by the front-end. The Spring-boot #PostMapping would accept the XML and JSON. I want to call different methods based on the Content-Type.
Is there a way to check what is the incoming content type?
#CrossOrigin(origins = "*")
#RestController
#RequestMapping("/api")
public class MyController {
#PostMapping(value = "/generator", consumes = {"application/json", "application/xml"}, produces = "application/json")
public String generate(#RequestBody String input) {
try {
System.out.println("INPUT CONTENT TYPE : ");
if(contentType == "application/xml")
{
//Call Method-1
}else if(contentType == "application/json"){
//Call Method-2
}
} catch (Exception exception) {
System.out.println(exception.getMessage());
}
}
}
As we can see the RestController method accepts XML and JSON. I want to check whats the incoming Content-type is based on its need to make different decisions. Can someone please explain to me how to do it?
Please Note:
I am aware that I can create different methods to handle XML and JSON but I would like to do it in a single method so it would be easy and efficient.
Add RequestHeader with its name Content-type:
public String generate(#RequestBody String input, #RequestHeader("Content-type") String contentType)
Annotation which indicates that a method parameter should be bound to a web request header.
You can use
#RequestHeader Map<String, String> headers
inside param of your generate() methode for get all Header come from the client.
After that, just check the
Content-Type
value

How to get headers of MultipartFile?

Using spring MVC I receive multipart files in the controller this way
#RestController
public class FilesController {
#PostMapping(path = ("/files"), consumes = {"multipart/form-data", "multipart/mixed"})
public Reference createFile(
#RequestPart(value = "description") FileDescription fileDesc,
#RequestPart(value = "attachments", required = false) List<MultipartFile> attachments) {
Some parts of the multipart request may contain headers like "Content-ID", "Content-Location" and so on. But spring interface MultipartFile doesn't provide a method to get any header I want, only getContentType as I see. How I can get all provided headers?
Important point is that in request I could have multipart/mixed as a part of multipart/form-data. So every part of the message has its own map of headers. If I use #RequestHeader, I can see main headers of the request, but there are no headers of a specific part of multipart.
There might be another way, but the one I know of is to ask for a MultipartHttpServletRequest in your method signature.
#PostMapping(path = ("/files"), consumes = {"multipart/form-data", "multipart/mixed"})
public Reference createFile(MultipartHttpServletRequest multipartRequest)
You can ask for other arguments if need be.
This object allows you to access details of the multipart in a finer-grained way. For example, you can access each part's header using getMultipartHeaders(String paramOrFileName). You also have methods to access the files content this way, so you would not typically need to keep you #RequestPart inside the method signature.
We can also use javax.servlet.http.Part instead of MultipartFile. Interface Part has getHeader method.
#RequestPart(value = "attachments", required = false) List<Part> attachments
You can get all request headers by using this
#RequestHeader Map<String,String> headers After that, you can search for the header you are looking for.
You can use #RequestHeader annotation to retrieve all the headers from the request, like this:
#RestController
public class FilesController {
#PostMapping(path = ("/files"), consumes = {"multipart/form-data", "multipart/mixed"})
public Reference createFile(
#RequestHeader Map<String, String> headersMap
) {
// Use headersMap here
}
or if you want a single header's value, then you can specify the name of the header in #RequestHeader annotation, like this:
#RestController
public class FilesController {
#PostMapping(path = ("/files"), consumes = {"multipart/form-data", "multipart/mixed"})
public Reference createFile(
#RequestHeader("Content-ID") String contentId,
#RequestHeader("Content-Location") String contentLocation,
) {
// Use contentId, contentLocation here
}

Spring Contracts: how to send a Collection of Strings as a RequestBody

A question about how to write a contract for a method annotated with #RequestBody taking a Collection of Strings as a parameter.
I have the following method:
#PostMapping(path = "/some/uri", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
#ApiOperation("GET with body")
public Response<Boolean> someMethod(#RequestParam(value = "key") final String key,
#RequestBody final Collection<String> numbers){
return some logic;
}
and I have written the following contract for testing purposes:
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return true"
request {
method POST()
url("/some/uri?key=NEW_KEY")
body'''["12345",
"00143"]'''
}
response {
status 200
headers {header 'Content-Type': 'application/json;charset=UTF-8'}
body '''true'''
}
I keep getting 415, the test cannot find my method, I guess my mistake might be in the way I send the collection of strings, I have tried some other options but did not succed.
I tried the suggestions above but unfortunately they both did not solve my issue. The reason I got 415 was that when I added a body to my request, the check was also made behind the scenes on the content type of the body, so I had to explicitly specify that the body was in json format also in the request:
request {
method POST()
url("/some/uri?key=NEW_KEY")
headers {header 'Content-Type': 'application/json;charset=UTF-8'}
body'''["12345",
"00143"]'''
}

Determine acceptable content types in ExceptionHandler

In a Spring application, I have an endpoint which normally returns an image (produces = MediaType.IMAGE_PNG_VALUE).
I also have #ExceptionHandler functions to handle various functions.
I'm trying to find a way to determine, from within the #ExceptionHandler, if the client will accept text/plain or text/json so in the event of an error I can return back one of those, or omit it if they are only expecting image/png.
How can I determine what acceptable content types I can return for a given request?
You can access the request to inspect headers and return an appropriate response. It is standard Content Negotiation.
Here's an example:
#ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {RuntimeException.class})
protected ResponseEntity<Object> handleMyException(RuntimeException ex, WebRequest request) {
List<String> acceptableMimeTypes = Arrays.asList(request.getHeaderValues(HttpHeaders.ACCEPT));
if (acceptableMimeTypes.contains(MediaType.TEXT_PLAIN_VALUE)) {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
.body("hello");
}
throw ex;
}
}
There are some arguments that spring-mvc can automagically inject into controller methods, and WebRequest (which is spring's representation of an http request) is one of those. If the client has sent an Accept : text/plain header with the request, the above example returns the string hello if there's a RuntimeException. If there's no exception, this logic won't get triggered at all, so the endpoint will just return whatever it normally returns. You can read more about #ControllerAdvice and #ExceptionHandler here.
Of course, be sure to think about the exact exception types you want to handle, and semantically appropriate status codes to return so that the clients know how to correctly interpret the response.
This is the answer I came up with. It's similar to YoungSpice's, but it is a little more flexible and uses MediaType directly (which means it'll handle wildcard types like text/* and the like):
private ResponseEntity<String> buildResponse(WebRequest request, HttpStatus status, String message) {
HttpHeaders httpHeader = new HttpHeaders();
List<MediaType> acceptHeader =
MediaType.parseMediaTypes(Arrays.asList(request.getHeaderValues(HttpHeaders.ACCEPT)));
if (acceptHeader.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.APPLICATION_JSON))) {
httpHeader.setContentType(MediaType.APPLICATION_JSON);
return new ResponseEntity<>("{ \"error\": \"" + message + "\" }", httpHeader, status);
} else if (acceptHeader.stream().anyMatch(mediaType -> mediaType.isCompatibleWith(MediaType.TEXT_PLAIN))) {
httpHeader.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>(message, httpHeader, status);
} else {
return ResponseEntity.status(status).body(null);
}
}
Basically, it uses MediaType.parseMediaTypes() to parse the Accept header, then I stream through them and use the mediaType.isCompatibleWith() function to check if my target is acceptable. This will let it handle if the header has something like application/* instead of application/json directly.
It also seems like if Accept isn't explicitly provided in the request, there is an implied */*, which seems to work as intended.

#ResponseBody unable to return XML

I have a REST Webservice returning an int via #responseBody and I want this response to be in XML, and I don't know how to achieve that despite many tries.
My controller is as follow:
#RequestMapping(value = "/UserByAppli", method = RequestMethod.GET)
#ResponseBody
public List<Application> getNbUserByAppli()
{
return this.DAO.getNbUserByAppli();
}
And my application Object:
#Component
#XmlRootElement(name="Application")
#XmlAccessorType(XmlAccessType.FIELD)
public class Application
{
#XmlElement(name="Nom")
private String name;
#XmlElement(name="NbUtilisateurs")
private int nbUsers;
public Application()
{
}
...
}
It always returns application/json, and when I specify the header "Accept=application/xml" I get a 406 response code with org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation in Spring logs.
An explanation or a search direction would be appreciated...
Make sure you have JAXB2 in your classpath and have registered the appropriate message converter and pass the Accept: application/xml header. Also, like M. Deinum suggested, for the marshalling to work, you also need to wrap the <Application /> elements in another element <Applications />.

Categories