Spring Boot manual content negotiation - java

I'm reworking old rest api and need to keep compatibility with it. Old api uses servlets and works with both xml and json. The logic is following:
it checks 'Content-Type' header, if it's supported ('text/xml', 'application/xml', 'application/json'), it's used as is;
if it's not supported (e.g. '*/*', 'text/plain', 'multipart/form-data') or not exists, 'application/xml' is used;
then 'Accept' header is checked in the same way with the only addition, if it's not present the same value as 'Content-Type' is used.
How can I achieve the same result with Spring MVC (using Spring Boot)? I tried to override configureContentNegotiation in config class, but it does not seem to work:
#SpringBootApplication
#EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
#ComponentScan(basePackages = {"..."})
public class AppConfiguration extends WebMvcConfigurerAdapter {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentTypeStrategy((NativeWebRequest request) -> {
String header = request.getHeader("Content-Type");
MediaType mediaType;
if (Objects.isNull(header)) {
mediaType = MediaType.APPLICATION_XML;
} else switch (header) {
case MediaType.TEXT_XML_VALUE:
case MediaType.APPLICATION_XML_VALUE:
case MediaType.APPLICATION_JSON_VALUE:
case MediaType.APPLICATION_JSON_UTF8_VALUE:
mediaType = MediaType.valueOf(header);
break;
default:
mediaType = MediaType.APPLICATION_XML;
}
return Arrays.asList(mediaType);
});
}
/*the rest of configuration*/
}

You can use HttpServletRequest for request header values.
#Autowire
HttpServletRequest request;
private String getContentType() {
return request.getHeader("Content-Type");
}

Related

How to set fixed headers to the feign client instead of setting on request level

I am using feign client for inter-service communication; question is I am able to send the method/request headers on request level meaning ex:
#FeignClient(name = "product-service", url = "https://jsonplaceholder.typicode.com/")
public interface ProductClient {
#GetMapping("/posts")
List<PostDTO> fetchPosts(#RequestHeaders....);
#GetMapping("/posts/{id}")
List<PostDTO> fetchPostsById(#RequestHeaders...., #PathVariable("id")int id);
But as header is fixed, instead of sending the same value to each request; can we set it on class level; I tried below; it is not working
#FeignClient(name = "product-service", url = "https://jsonplaceholder.typicode.com/")
#Headers({
"X-Ping: {token}"
})
public interface ProductClient {
#GetMapping("/posts")
List<PostDTO> fetchPosts(#RequestHeaders....);
#GetMapping("/posts/{id}")
List<PostDTO> fetchPostsById(#RequestHeaders...., #PathVariable("id")int id);
Correct me with the API or an example.
You can create an interceptor that'd inject headers in all requests as below:
#Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("user", username);
requestTemplate.header("password", password);
requestTemplate.header("Accept", ContentType.APPLICATION_JSON.getMimeType());
};
}
Also it provides a way to set interceptor using property files as below:
feign:
client:
config:
default:
requestInterceptors:
com.baeldung.cloud.openfeign.JSONPlaceHolderInterceptor
we can create the configuration with default as the client name to configure all #FeignClient objects, or we can declare the feign client name for a configuration
Reference: https://www.baeldung.com/spring-cloud-openfeign
EDIT:
Another way is to set headers in yml as below:
feign:
client:
config:
default:
defaultRequestHeaders:
Authorization:
- Basic dXNlcjpwYXNzd29yZA==
SomeOtherHeader:
- Value1
- Value2
we have used below for kotlin to make sure we add proper headers to all the request sent :
Feign client with configuration setup
#FeignClient(name = "YourClient", url = "\${base-url}", configuration = [FeignHeaderConfiguration::class])
interface YourClient
Header configuration
#Configuration
class FeignHeaderConfiguration {
#Bean
fun clientHeaderInterceptor(): ClientHeaderInterceptor {
return ClientHeaderInterceptor()
}
}
and interceptor where we added the header
class ClientHeaderInterceptor : RequestInterceptor {
override fun apply(requestTemplate: RequestTemplate) {
requestTemplate.header("Accept", MediaType.APPLICATION_JSON_VALUE)
}
}

Authorization in JAX-RS

I am developing an application using javaEE / Wildfly and JAX-RS for the restful service.
I have this kind of endpoint :
#POST
#Path("/add")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public Response addSchool(SchoolDto schoolDto, #HeaderParam("token") String userToken) {
List<String> actionsNeeded = new ArrayList<String>(
Arrays.asList(
"create school"
));
if (authService.userHasActionList(userToken, actionsNeeded) == false )
{
return authService.returnResponse(401);
}
Response addSchoolServiceResponse = schoolResponse.create(schoolDto);
return addSchoolServiceResponse;
}
Using the token in Header my auth service will check if the user account has, in his list of authorized actions, those that are necessary to use the checkpoint.
It's working, but I'm repeating that on each checkpoint ... I'm looking for a way to do that :
#POST
#Path("/add")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#Annotation("action 1 needed", "Action 2 needed")
public Response addSchool(SchoolDto schoolDto, #HeaderParam("token") String userToken) {
Response addSchoolServiceResponse = schoolResponse.create(schoolDto);
return addSchoolServiceResponse;
}
an annotation where i can pass some parameters (my actions and most important be able to have the user token) who trigger using filter or whatever the security check return a 401 or let the method to be executed if user is allowed to be there.
I've find a lot of stuff (#Secured etc...) for security based on role but not on action like that
Is someone already did something like that ?
Finally I've started all over and it's working, my principal problem was to access token in the header and working with annotations and it's ok now (just need to insist and try one more time i assume ...) here is what it's look likes :
#Provider
#Actions
public class AuthorizationFilter implements ContainerRequestFilter {
#EJB
AuthService authService;
#Context
private ResourceInfo resourceInfo;
List<String> actionsNeeded = new ArrayList<String>();
#Override
public void filter(ContainerRequestContext reqContext) throws IOException {
Actions annotations = resourceInfo.getResourceMethod().getAnnotation(Actions.class);
String token;
try {
token = reqContext.getHeaders().get("token").get(0);
for (String annotation : annotations.value()) {
actionsNeeded.add(annotation);
}
if (authService.userHasActionList(token, actionsNeeded) == false )
{
reqContext.abortWith(authService.returnResponse(401));
return;
}
} catch (Exception e) {
System.out.println("Headers 'token' does not exist !");
reqContext.abortWith(authService.returnResponse(400));
}
}
}

How to serve already gzipped content in JAX-RS?

I'm developing a small JAX-RS application with Resteasy. I wanted the application to serve some static content for Javascript and CSS files, etc. and I would like to take advantage of the already gzipped version of the resources packaged in the jars of webjars.org. Thus, I need to handle the Accept-Encoding header and check if the .gz is there (or not).
So far, what I have is:
#Path("res/{path:.*}")
#GET
public Response webjars(#PathParam("path") String path, #HeaderParam("Accept-Encoding") String acceptEncoding) {
// Guesses MIME type from the path extension elsewhere.
String mime = mimes.getContentType(path);
if (acceptEncoding.contains("gzip")) {
InputStream is = getClass().getResourceAsStream("/META-INF/resources/webjars/" + path + ".gz");
if (is != null)
return Response.ok().type(mime).encoding("gzip").entity(is).build();
}
InputStream is = getClass().getResourceAsStream("/META-INF/resources/webjars/" + path);
if (is != null)
return Response.ok().type(mime).entity(is).build();
return Response.status(Status.NOT_FOUND).build();
}
But it doesn't work. The content served is totally broken. So far, I've found that a component that compresses the stream again: org.jboss.resteasy.plugins.interceptors.encoding.GZIPEncodingInterceptor because I manually filled the Content-Encoding header (using the ResponseBuilder.encoding method).
This looks like a bug to me because, apparently, there's no way to share an already gzipped stream. However, Is this achievable using JAX-RS? Is this a Resteasy bug?
I can think of a variety of ways to achieve the same thing externally to Resteasy, like mapping the webjars.org servlet (I'm not in a Servlet API 3.0 environment, so I have no META-INF/resources/ automatic classpath mapping). Nevertheless, my questions still prevail. It applies to several other scenarios.
Update:
For the record I have filled the issue RESTEASY-1170.
Here's an example implementation of my above comment.
The point I'm getting at is that if you don't want it to be handle by the current interceptor, don't set the header, create an Interceptor that will be name binded, with your own annotation, and set the priority to one lower than the one you want to avoid, then set the header in your Interceptor...
#AlreadyGzipped
#NameBinding
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface AlreadyGzipped {}
WriterInterceptor. Notice the #Priority. The GZIPEncodingInterceptor uses Priorities.ENTITY_CODER
#Provider
#AlreadyGzipped
#Priority(Priorities.ENTITY_CODER + 1000)
public class AlreadyGzippedWriterInterceptor implements WriterInterceptor {
#Context HttpHeaders headers;
#Override
public void aroundWriteTo(WriterInterceptorContext wic) throws IOException,
WebApplicationException {
String header = headers.getHeaderString("Accept-Encoding");
if (null != header && header.equalsIgnoreCase("gzip")) {
wic.getHeaders().putSingle("Content-Encoding", "gzip");
}
wic.proceed();
}
}
Test resource
#Path("resource")
public class AlreadyGzippedResoure {
#GET
#AlreadyGzipped
#Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response getAlreadGzipped() throws Exception {
InputStream is = getClass().getResourceAsStream("/stackoverflow.png.gz");
return Response.ok(is).build();
}
}
Test
public class Main {
public static void main(String[] args) throws Exception {
Client client = ClientBuilder.newClient();
String url = "http://localhost:8080/api/resource";
Response response = client.target(url).request().acceptEncoding("gzip").get();
Image image = ImageIO.read(response.readEntity(InputStream.class));
JOptionPane.showMessageDialog(null,new JLabel(new ImageIcon(image)));
}
}
Result

Spring MVC response encoding issue

In last few hours I've read a lot concerning this topic, and so far nothing has worked. I'm trying to return response containing "odd" some characters. Here is example of that, quite simple :
#ResponseBody
#RequestMapping(value="test")
public String test(){
String test = "čćžđš";
System.out.println(test);
logger.info(test);
return test;
}
This is my web.xml, because I found some answers where CharacterEncodingFilter helped(not in my case though). I used POST method because I read this applies to POST.
Also found this answer(related). Didn't help as well.
When I debug it the correct value appears, but when I print it doesn't as it can be seen below:
When I test it from jmeter, the response seems to be OK, Content-Type is text/html;charset=UTF-8
Here is a screenshot of that as well. http://i56.tinypic.com/14lt653.jpg
I think the right way is to return UTF-8, maybe I'm wrong.
Instead #ResponseBody use ResponseEntity.
#RequestMapping(value="test")
public ResponseEntity<String> test(){
String test = "čćžđš";
System.out.println(test);
logger.info(test);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("Content-Type", "application/json; charset=UTF-8");
return ResponseEntity<String>(test,responseHeaders, HttpStatus.OK);
}
After few days of this I just had "who's your daddy moment". It came from reading spring 3.0 reference, I had nothing else to try so why not go trough entire documentation.. and combination of #axtavt answer :
Who sets response content-type in Spring MVC (#ResponseBody)
Changed original solution :
public class EncodingPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name)
throws BeansException {
if (bean instanceof AnnotationMethodHandlerAdapter) {
HttpMessageConverter<?>[] convs = ((AnnotationMethodHandlerAdapter) bean).getMessageConverters();
for (HttpMessageConverter<?> conv: convs) {
if (conv instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) conv).setSupportedMediaTypes(
Arrays.asList(new MediaType("text", "html",
Charset.forName("UTF-8"))));
}
}
}
return bean;
}
To :
public class EncodingPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name)
throws BeansException {
if (bean instanceof AnnotationMethodHandlerAdapter) {
HttpMessageConverter<?>[] convs = ((AnnotationMethodHandlerAdapter) bean).getMessageConverters();
for (HttpMessageConverter<?> conv: convs) {
if (conv instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) conv).setSupportedMediaTypes(
Arrays.asList(new MediaType("text", "plain",
Charset.forName("UTF-8"))));
}
}
}
return bean;
}
Darn spring!!! but still I'll continue to use it.
My simple solution:
#RequestMapping(value="test")
public ModelAndView test(){
String test = "čćžđš";
...
ModelAndView mav = new ModelAndView("html_utf8");
mav.addObject("responseBody", test);
}
and the view html_utf8.jsp
<%# page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>${responseBody}
No additional classes and configuration.
And You can also create another view (for example json_utf8) for other content type.
I can see two problems in the actual delivered response.
The response is clearly just text, but your response content-type header is saying it is HTML.
Judging from the content-length of the response, the content has not actually been encoded in UTF-8.
FWIW - the CharacterEncodingFilter won't help with your problem because it is dealing with the encoding of the request not the response.
I think that the problem is that you need to configure the message converter for the response body. However, it appears that your application already does something in this area, because the default behavior of the StringHttpMessageConverter is to use "text/plain" as its content type.

Ignore Accept header in spring mvc

I have a controller that serves files (images, pdfs, etc,.):
#Controller
public class FileController {
#ResponseBody
#RequestMapping("/{filename}")
public Object download(#PathVariable String filename) throws Exception {
returns MyFile.findFile(filename);
}
}
If I request a file with the following Accept header I get a 406:
Request
URL: http://localhost:8080/files/thmb_AA039258_204255d0.png
Request Method:GET
Status Code:406 Not Acceptable
Request Headers
Accept:*/*
If I request the same file with the following Accept header I get a 200:
URL: http://localhost:8080/files/thmb_AA039258_204255d0.png
Request Method: GET
Status Code:200 OK
Request Headers
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
This is the only view resolver in my spring-mvc context:
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" id="tilesViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
</bean>
Is there anyway to configure spring mvc to ignore the Accept header? I've seen example of doing this with ContentNegotiatingViewResolver, but only for handling xml and json.
So this is the code I ended up with to get it working:
#Controller
public class FileController {
#ResponseBody
#RequestMapping("/{filename}")
public void download(#PathVariable String filename, ServletResponse response) throws Exception {
MyFile file = MyFile.find(filename);
response.setContentType(file.getContentType());
response.getOutputStream().write(file.getBytes());
}
}
I used this to lock to the JSON response type:
#Configuration
#EnableWebMvc
public class ApplicationConfiguration extends WebMvcConfigurerAdapter {
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(false);
configurer.ignoreAcceptHeader(true);
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
favorPathExtension(false) is needed because Spring by default (at least in 4.1.5) favors path-based content negotiation (i.e. if the URL ends with ".xml", it will try to return XML, etc.).
When you use ResponseBody annotation, I think that is part of the deal that it looks at the Accept header and tries to do some mapping or whatever. There are plenty of other ways to send a response though if you can't figure out how to do it with that annotation.

Categories