Spring webclient add httpmessageconverter - spring

I want to convert spring resttemplate to spring webclient.
in spring resttemplate we can add message converters
How can I tell RestTemplate to POST with UTF-8 encoding?
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
How can I do it in spring webclient? I want to add message converter to webclient

In WebClient you can setup custom codecs(Encoder, Decoder, HttpMessageReader, HttpMessageWriter) using WebClient.builder().codecs()
Here is an example:
WebClient.builder()
.codecs(
clientCodecConfigurer ->{
// .defaultCodecs() set defaultCodecs for you
// clientCodecConfigurer.defaultCodecs();
// You can customize an encoder based on the defualt config.
// clientCodecConfigurer.defaultCodecs().jackson2Encoder(...)
// Or
// use customCodecs to register Codecs from scratch.
clientCodecConfigurer.customCodecs().register(new Jackson2JsonDecoder());
clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
}
).build();

Related

How can SOAP be used with Spring Reactor's WebClient?

I've managed to build an SSL connection to the sandbox server and to send the object as a serialised XML object by applying the content type MediaType.APPLICATION_XML. However, this is not enough as the target service only supports SOAP and expects the message properly wrapped in an envelope.
final var webClient = WebClient.builder()
.baseUrl(fmdConfiguration.getSinglePackUrl())
.clientConnector(connector)
.exchangeStrategies(exchangeStrategies)
.filter(logResponseStatus())
.filter(logRequest())
.build();
return webClient
.method(GET)
.contentType(MediaType.APPLICATION_XML)
.body(BodyInserters.fromObject(request))
.retrieve()
.bodyToMono(SinglePackPingResponse.class);
This is the response from the service:
Unable to create envelope from given source because the root element is not named "Envelope"
Unfortunately the the WebClient doesn't support the media type application/soap+xml. When I try to use it, then the WebClient throws the following error:
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/soap+xml;charset=UTF-8' not supported for bodyType=eu.nmvs.SinglePackPingRequest
at org.springframework.web.reactive.function.BodyInserters.unsupportedError(BodyInserters.java:300)
I use:
private void acceptedCodecs(ClientCodecConfigurer clientCodecConfigurer) {
clientCodecConfigurer.customCodecs().encoder(new Jackson2JsonEncoder(new ObjectMapper(), TEXT_XML));
clientCodecConfigurer.customCodecs().decoder(new Jackson2JsonDecoder(new ObjectMapper(), TEXT_XML));
}
and:
webClient = webClientBuilder
.baseUrl(baseUrL)
.filter(logRequest())
.exchangeStrategies(ExchangeStrategies.builder().codecs(this::acceptedCodecs).build()).build();

How to apply Spring Cloud Gateway GlobalFilter on WebClient in a RouterFuncation bean?

Currently I am trying to use Spring Cloud Gateway(Spring Cloud version: Finchley.M5) in a edge-service, my sample has a Spring Session header(X-AUTH-TOKEN) token based authentication.
In Gateway specific RouteLocator, the authentication works well, because the built-in GlobalFilters are applied on the RouteLocator when it passes request to downstream microservice.
But I want to create a generic RouterFunction to consume some resources of downstream services and aggregate them together in a new edgeservice, the GlobalFileters will not apply on my webclient bean.
#Bean
RouterFunction<ServerResponse> routes(WebClient webClient) {
log.debug("authServiceUrl:{}", this.authServiceUrl);
log.debug("postServiceUrl:{}", this.postServiceUrl);
log.debug("favoriteServiceUrl:{}", this.favoriteServiceUrl);
return route(
GET("/posts/{slug}/favorited"),
(req) -> {
Flux<Map> favorites = webClient
.get()
.uri(favoriteServiceUrl + "/posts/{slug}/favorited", req.pathVariable("slug"))
.retrieve()
.bodyToFlux(Map.class);
Publisher<Map> cb = from(favorites)
.commandName("posts-favorites")
.fallback(Flux.just(Collections.singletonMap("favorited", false)))
.eager()
.build();
return ok().body(favorites, Map.class);
}
)
...
Is there a simple solution to apply Gateway Filter also work on the RouterFunction, thus my header token based authentication can work automatically?
Or there is a simple way to pass current X-AUTH-TOKEN header into the downstream webclient request headers?
In traditional MVC, there is a RequestContext used to get all headers from current request context and pass them to the downstream request in a global filter. Is there an alternative of RequestContext in webflux to read all headers from current request context?
The complete codes is here.

OAuth2 Client should send custom properties

I have implemented Oauth2 client in spring boot
public RestTemplate oAuthRestTemplate() {
ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setId("1");
resourceDetails.setClientId(clientId);
resourceDetails.setClientSecret(clientSecret);
resourceDetails.setAccessTokenUri(accessTokenUrl);
resourceDetails.setTokenName("accessToken");
resourceDetails.setClientAuthenticationScheme(AuthenticationScheme.form);
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(resourceDetails, new DefaultOAuth2ClientContext());
return restTemplate;
}
We I run the code, request body for token is as
client_id & client_secret (by default)
Can we send is custome manner.
like I want to send it as clientId & clientSecret.
Note: Class is annotated with #EnableOAuth2Client
The names 'client_id' and 'client_secret' are specified by OAuth 2
So there is normally no need to change these.
However, Spring uses org.springframework.security.oauth2.client.token.auth.DefaultClientAuthenticationHandler
to post the client_id and client_secret. There you can see the field names are hardcoded :
form.set("client_id", resource.getClientId());
if (StringUtils.hasText(clientSecret)) {
form.set("client_secret", clientSecret);
}
You could try to subclass DefaultClientAuthenticationHandler, and use your own field names. But as it is created with 'new' in OAuth2AccessTokenSupport, you would need to subclass this also, and so on......
This can be tricky, a better way may be to add a
org.springframework.http.client.ClientHttpRequestInterceptor
at the org.springframework.security.oauth2.client.OAuth2RestTemplate
Create a class that implements ClientHttpRequestInterceptor, in its method
org.springframework.http.client.ClientHttpRequestInterceptor#intercept
you need to create a new request using the provided body of type byte[].
Convert it to a String, replace the field names, create a new request using it and proceed as described in the javadoc of the intercept() method.
To register the interceptor, create a spring bean of type OAuth2RestTemplate and register the interceptor there like
#Bean
OAuth2RestTemplate oAuth2RestTemplate(){
OAuth2RestTemplate template=new OAuth2RestTemplate;
ClientHttpRequestInterceptor interceptor= new ... //create your interceptor here
template.setInterceptors(Arrays.asList(interceptor));
return template;
}

Spring Boot RequestMapping with non-standard produces value returning 406 error when returning JAXB annotated object

I'm creating a Spring Boot app to replace a legacy api application, so all the routes/headers/etc are already set in stone. In that legacy app we used custom Accept headers to include both the version and the content type. So our Accept header is like:
catalog.v1.xml or catalog.v2.json etc.
Here is my request mapping for the method that is handling the request. I'm trying to handle the v1.xml one now. Spring is finding the correct method and the whole method is executed and it returns my JAXB annotated object:
#RequestMapping(value = "/catalog", method = RequestMethod.GET, produces="application/catalog.v1.xml")
How do I make sure Spring finds this matching handler method based on my Accept header, but knows that the output should be XML and marshall my JAXB object accordingly?
You need to provide Spring MVC with an HttpMessageConverter for your custom media type. To do so, I'd take advantage of Spring Boot automatically adding any HttpMessageConverter beans to Spring MVC's default configuration by configuring a bean that knows how to convert application/catalog.v1.xml:
#Bean
public Jaxb2RootElementHttpMessageConverter catalogXmlConverter() {
Jaxb2RootElementHttpMessageConverter xmlConverter = new Jaxb2RootElementHttpMessageConverter();
xmlConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("application", "catalog.v1.xml")));
return xmlConverter;
}
So once I realized I was changing the wrong configuration, and from deep debugging into Spring code, I realized I needed to replace or modify the message converter behavior. Here is my solution below. They don't make it super easy. If anyone has a simpler way of doing this, please let me know. This works.
public void extendMessageConverters(List<HttpMessageConverter<?>> converters)
{
for (HttpMessageConverter<?> converter : converters) {
if (converter.getClass() == MappingJackson2HttpMessageConverter.class){
MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter)converter;
MediaType jsonType = new MediaType("application", "catalog.v2.json");
MediaType jsonpType = new MediaType("application", "catalog.v2.jsonp");
List<MediaType> mediatTypes = new ArrayList<>(jacksonConverter.getSupportedMediaTypes());
mediatTypes.add(jsonpType);
mediatTypes.add(jsonType);
jacksonConverter.setSupportedMediaTypes(mediatTypes);
}
else if (converter.getClass() == Jaxb2RootElementHttpMessageConverter.class){
Jaxb2RootElementHttpMessageConverter xmlConverter = (Jaxb2RootElementHttpMessageConverter) converter;
MediaType xmlType = new MediaType("application", "catalog.v1.xml");
// Since the SupportMediaTypes list is unmodifiable, we have to create a new one based on it
// and replace it completely
List<MediaType> mediatTypes = new ArrayList<>(xmlConverter.getSupportedMediaTypes());
mediatTypes.add(xmlType);
xmlConverter.setSupportedMediaTypes(mediatTypes);
}
}
}

Spring RestTemplate with Jackson throws "Can not resolve BeanPropertyFilter" when using #JsonFilter

Can I specify the Jackson ObjectMapper that Spring's RestTemplate uses?
I'm not 100% that's what I need to do but see below for details.
Background:
With help from this StackOverflow post I added #JsonFilter to my domain class and edited my jax-rs web service (implemented in CXF). I'm now successfully able to dynamically select which domain class fields to return in my RESTful API. So far so good.
I'm using Spring's RestTemplate in my JUnit tests to test my RESTful API. This was working fine until I added #JasonFilter to my domain class. Now I'm getting the following exception:
org.springframework.web.client.ResourceAccessException: I/O error: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured; nested exception is org.codehaus.jackson.map.JsonMappingException: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:453)
rest of stack trace omitted for brevity
Caused by: org.codehaus.jackson.map.JsonMappingException: Can not resolve BeanPropertyFilter with id 'apiFilter'; no FilterProvider configured
at org.codehaus.jackson.map.ser.BeanSerializer.findFilter(BeanSerializer.java:252)
I was getting a similar problem on the server side and was able to resolve it (with help from this post) by giving a FilterProvider to the Jackson ObjectMapper as follows:
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.filterOutAllExcept(filterProperties));
Can I do something similar on the RestTemplate side? Any ideas of how to solve this issue are appreciated.
Just to be clear, on the client RestTemplate side I do not want to filter the domain object properties at all.
Can I specify the Jackson ObjectMapper that Spring's RestTemplate uses?
I was able to force RestTemplate to use a customized ObjectMapper by doing the following:
ObjectMapper mapper = new ObjectMapper();
// set a custom filter
Set<String> filterProperties = new HashSet<String>();
FilterProvider filters = new SimpleFilterProvider().addFilter("apiFilter", SimpleBeanPropertyFilter.serializeAllExcept(filterProperties));
mapper.setFilters(filters);
MappingJacksonHttpMessageConverter messageConverter = new MappingJacksonHttpMessageConverter();
messageConverter.setObjectMapper(mapper);
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
messageConverters.add(messageConverter);
restTemplate.setMessageConverters(messageConverters);
This website provided example for part of the above code.
Just adding to the answer. If you are using TestRestTemplate then you can actually get the underlying RestTemplate class and then modify its MappingJackson2HttpMessageConverter to include your filter:
var jackson2HttpMessageConverter = testRestTemplate.getRestTemplate().getMessageConverters().stream()
.filter(mc -> mc instanceof MappingJackson2HttpMessageConverter)
.map(mc -> (MappingJackson2HttpMessageConverter) mc)
.findFirst()
.orElseThrow();
jackson2HttpMessageConverter.getObjectMapper().setFilterProvider(
new SimpleFilterProvider().addFilter("MyFilterName", SimpleBeanPropertyFilter.serializeAll())
);

Resources