Currently a request method receives a encoded token as parameter (this token is a String but not JSON or anything like that).
This token then gets decoded to a data class.
#GetMapping(value = "/api/xyz")
public ResponseEntity<XYZ> xyz(#NotBlank String token) {
Data data = Token.parse(token);
...
}
Is it possible to write a annotation similar to how #RequestBody works that converts a parameter into something else?
The request should just look like that instead:
#GetMapping(value = "/api/xyz")
public ResponseEntity<XYZ> xyz(#TokenParser Data data) {
...
}
This class / annotation should contain the code necessary to convert the token (String) to my data class.
The request method should just have the converted data class as parameter.
I solved it with the spring-boot Converter.
import javax.inject.Inject;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
#Component
public class TokenConverter implements Converter<String, Data> {
private final TokenParser tokenParser;
#Inject
public TokenConverter(TokenParser TokenParser) {
this.tokenParser = tokenParser;
}
#Override
public Data convert(String token) {
return tokenParser.parse(token);
}
}
Just add such a converter anywhere in your project.
The mapping is managed by spring-boot.
New request:
#GetMapping(value = "/api/method")
public ResponseEntity<Data> method(#RequestParam("token") Data data) {
...
}
For more information: https://www.baeldung.com/spring-mvc-custom-data-binder
I hope it helps someone else.
Related
I have spent the last 2 days trying every possible way of modifying the response body of a request before it hits the client, and nothing seems to work for me. So far I have tried the implementations mentioned here, here, here, here, here and a few others that I can't find right now, but nothing has worked. It doesn't matter if I define the filter as pre, post, global, gateway or route-specific - the actual response modification doesn't seem to work for me.
My situation is the following:
I have a YAML-configured API gateway running and have configured one of its routes to lead to an ADF service in the background. The issue I have with this ADF application is that the response it returns to the client is in the form of an HTML template that is automatically generated by its backend. In this template, some of the URLs are hardcoded and point to the address of the application itself. To justify the use of an API Gateway in this case, I want to replace those ADF URLs with those of the API Gateway.
For simplicity's sake, let's say the IP address of my ADF service is 1.2.3.4:1234, and the IP address of my API Gateway is localhost:8080. When I hit the ADF route in my gateway, the response contains some auto-generated javascript inserts, such as this one:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://1.2.3.4:1234/entry/dynamic/index.jspx");
As you can see, it contains a hardcoded URL. I want to access the response body and find all those hardcoded URLs and replace them with the gateway URL, so the above example becomes:
AdfPage.PAGE.__initializeSessionTimeoutTimer(1800000, 120000, "http://localhost:8080/entry/dynamic/index.jspx");
To do this, it seems sensible to me to have a global POST filter that kicks in only when the request matches the route for my ADF application, so that's what I've settled on doing.
Here is my post filter so far:
#Bean
public GlobalFilter globalADFUrlReplacementFilter() {
return (exchange, chain) -> chain.filter(exchange).then(Mono.just(exchange)).map(serverWebExchange -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (requestIsTowardsADF(request)) {
logger.info("EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT");
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {
#Override
#SuppressWarnings("unchecked")
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
logger.info("OVERRIDING writeWith METHOD TO MODIFY THE BODY");
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(buffer -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(buffer);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
String bodyStr = new String(content, StandardCharsets.UTF_8);
bodyStr = bodyStr.replace(ADF_URL, API_GATEWAY_URL);
getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
return bufferFactory().wrap(bodyStr.getBytes());
}));
}
};
logger.info("ADF URL REPLACEMENT FILTER DONE");
return chain.filter(serverWebExchange.mutate().request(request).response(responseDecorator).build());
}
return serverWebExchange;
})
.then();
}
And the config:
spring:
cloud:
gateway:
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
You can see that I'm using a org.slf4j.Logger object to log messages in the console. When I run my API Gateway and hit the ADF route, I can see the following:
EXECUTING GLOBAL POST FILTER FOR ADF TEMPLATE URL REPLACEMENT
ADF URL REPLACEMENT FILTER DONE
And when I check the response I got back from the API Gateway, I can see that the response body is still identical and the ADF URLs have not been replaced at all. I tried debugging the application and as soon as it reaches ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) { it skips over the entire anonymous class implementation within those curly braces. A testament to that is the absence of the OVERRIDING writeWith METHOD TO MODIFY THE BODY log in the console - it never got executed!
It seems that for some reason the actual body modification doesn't get executed and I can't figure out why. I tried several different implementations of this filter, as mentioned in the above links, and neither of them worked.
Can someone please share with me a working POST filter that modifies the response body, or point out the flaw in my solution?
Thanks a bunch in advance!
Thanks for sharing this sample filter cdan. I provided the most straightforward solution to my issue using it as a template. Here's how it looks:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
#Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
final Logger logger = LoggerFactory.getLogger(TestFilter2.class);
public static class Config {
private String param1;
public Config() {
}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
}
#Override
public List<String> shortcutFieldOrder() {
return List.of("param1");
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public TestFilter2() {
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
#Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
I have added this filter to my route definition like so:
spring:
cloud:
gateway:
httpclient:
wiretap: true
httpserver:
wiretap: true
routes:
- id: adf-test-2
uri: http://1.2.3.4:1234
predicates:
- Path=/entry/**
filters:
- TestFilter2
I'm simply trying to modify the response body and replace the ADF URL in it with the AG URL, but whenever I try to hit the ADF route I get the below exception:
2022-05-08 17:35:19.492 ERROR 87216 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [284b180d-1] 500 Server Error for HTTP GET "/entry/dynamic/index.jspx"
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String
at org.springframework.web.reactive.function.BodyExtractors.lambda$readWithMessageReaders$12(BodyExtractors.java:201) ~[spring-webflux-5.3.18.jar:5.3.18]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? Body from UNKNOWN [DefaultClientResponse]
*__checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ? HTTP GET "/entry/dynamic/index.jspx" [ExceptionHandlingWebHandler]
I searched the web for some time but wasn't able to find any clear answer on why this UnsupportedMediaTypeException: Content type 'text/html' not supported for bodyType=java.lang.String exception gets thrown when I try to work with the bodyAsString field that is supposed to contain the response body as String. Debugging the entire filter didn't work either, as the exception seems to be thrown immediately after I hit the route and I can't even get in the body of that class. Am I missing something obvious?
UPDATE (09.05.2022):
After looking into this further, I refactored the filter structure a bit by removing the unnecessary parameter in the config, and Autowiring the dependency towards ModifyResponseBodyGatewayFilterFactory, and now it seems the filter works properly and does the replacement I needed it to do. I will test it a bit longer to make sure it does indeed work as expected, and if it does, I'll mark this as the solution. Thanks for all of your input cdan!
Here's the entire filter:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
#Component
public class TestFilter2 extends AbstractGatewayFilterFactory<TestFilter2.Config> {
public static final String ADF_URL = "1.2.3.4:1234";
public static final String AG_URL = "localhost:8080";
#Autowired
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public static class Config {
public Config() {
}
}
public TestFilter2(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
super(Config.class);
this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
}
#Override
public GatewayFilter apply(Config config) {
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> Mono.just(bodyAsString.replace(ADF_URL, AG_URL)));
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
Try with the built-in ModifyResponseBody Filter with Java DSL. If you still need more advanced response processing, your next option is to extend the ModifyResponseBodyGatewayFilterFactory class.
(Update 2022-05-08)
For example, using the Delegation design pattern (wrapping the built-in ModifyResponseBodyFilter in a new custom filter taking one custom parameter):
package test;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.*;
#Component
public class MyFilterFactory extends AbstractGatewayFilterFactory<MyFilterFactory.Config>
{
public static class Config
{
private String param1;
// Add other parameters if necessary
public Config() {}
public void setParam1(String param1) {
this.param1 = param1;
}
public String getParam1() {
return param1;
}
// Add getters and setters for other parameters if any
}
#Override
public List<String> shortcutFieldOrder()
{
return Arrays.asList("param1" /*, other parameters */ );
}
private final ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
public MyFilterFactory()
{
super(Config.class);
this.modifyResponseBodyFilterFactory = new ModifyResponseBodyGatewayFilterFactory(new ArrayList<>(), new HashSet<>(), new HashSet<>());
}
#Override
public GatewayFilter apply(Config config)
{
final ModifyResponseBodyGatewayFilterFactory.Config modifyResponseBodyFilterFactoryConfig = new ModifyResponseBodyGatewayFilterFactory.Config();
modifyResponseBodyFilterFactoryConfig.setNewContentType(MediaType.TEXT_HTML_VALUE);
modifyResponseBodyFilterFactoryConfig.setRewriteFunction(String.class, String.class, (exchange, bodyAsString) -> {
final String output;
/*
Do whatever transformation of bodyAsString (response body as String) and assign the result to output...
*/
return Mono.just(output);
});
return modifyResponseBodyFilterFactory.apply(modifyResponseBodyFilterFactoryConfig);
}
}
I'm trying to build a simple app that calls an API with quarkus-rest-client.
I have to inject an API Key as a header which is the same for all resources of the API.
So I would like to put the value of this API Key (that depends on the environment dev/qa/prod) in the application.properties file located in src/main/resources.
I tried different ways to achieve this:
Use directly com.acme.Configuration.getKey into #ClientHeaderParam value property
Create a StoresClientHeadersFactory class which implements ClientHeadersFactory interface to inject the configuration
Finally, I found the way described below to make it work.
My question is: Is there a better way to do it?
Here's my code:
StoreService.java which is my client to reach the API
#Path("/stores")
#RegisterRestClient
#ClientHeaderParam(name = "ApiKey", value = "{com.acme.Configuration.getStoresApiKey}")
public interface StoresService {
#GET
#Produces("application/json")
Stores getStores();
}
Configuration.java
#ApplicationScoped
public class Configuration {
#ConfigProperty(name = "apiKey.stores")
private String storesApiKey;
public String getKey() {
return storesApiKey;
}
public static String getStoresApiKey() {
return CDI.current().select(Configuration.class).get().getKey();
}
}
StoresController.java which is the REST controller
#Path("/stores")
public class StoresController {
#Inject
#RestClient
StoresService storesService;
#GET
#Produces(MediaType.APPLICATION_JSON)
public Stores getStores() {
return storesService.getStores();
}
}
Late to the party, but putting this here for my own reference. There seems to be a difference in using #ClientHeaderParam and #HeaderParam, so I investigated a little further:
According to the Microprofile docs, you can put a compute method for the value in curly braces. This method can extract the property value.
See link for more examples.
EDIT: What I came up with resembles the original, but uses a default method on the interface, so you can at least discard the Configuration class. Also, using the org.eclipse.microprofile.config.Config and ConfigProvider classes to get the config value:
#RegisterRestClient
#ClientHeaderParam(name = "Authorization", value = "{getAuthorizationHeader}")
public interface StoresService {
default String getAuthorizationHeader(){
final Config config = ConfigProvider.getConfig();
return config.getValue("apiKey.stores", String.class);
}
#GET
#Produces("application/json")
Stores getStores();
I will get rid of the Configuration class and use an #HeaderParam to pass your configuration property from your rest endpoint to your rest client. The annotation will then send this property as an HTTP header to the remote service.
Somthing like this should works:
#Path("/stores")
#RegisterRestClient
public interface StoresService {
#GET
#Produces("application/json")
Stores getStores(#HeaderParam("ApiKey") storesApiKey);
}
#Path("/stores")
public class StoresController {
#ConfigProperty(name = "apiKey.stores")
private String storesApiKey;
#Inject
#RestClient
StoresService storesService;
#GET
#Produces(MediaType.APPLICATION_JSON)
public Stores getStores() {
return storesService.getStores(storesApiKey);
}
}
When i go (hit URL in the browser) to some URL
I got a json response matching my Class fields (GET RESPONSE).
I want to converter this json to my object without using some Java converters like jackson.
What spring annotation could i use here?
What i try to do is something like that, so it automatically translates json to object:
#RequestMapping("/getCar")
public Car getCar(#SOMEANNOTATION(Car car)
{ return car;}
you can do it using RestTemplate:
RestTemplate restTemplate = new RestTemplate();
Car car = restTemplate.getForObject("http://localhost:8080/getCar", Car.class);
you need a Class car, so Spring can map it:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Car {
...
}
but you need to understand, that Spring will still use some converter under the hood to create/read the data...
you can try this #Consumes({ "application/json", "application/xml" })
There's another solution from Sring Boot documentation Building a RESTful Web Service.
Basically, the serialization/deserialization of the object is handled automatically by Spring converter. In other word, you have nothing to do in this case.
Consider a basic REST controller :
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
#RequestMapping("/greeting")
public Greeting greeting(#RequestParam(value="name", defaultValue="World") String name) {
return new Greeting(counter.incrementAndGet(),
String.format(template, name));
}
}
Explanation
The Greeting object must be converted to JSON. Thanks to Spring’s HTTP
message converter support, you don’t need to do this conversion
manually. Because Jackson 2 is on the classpath, Spring’s
MappingJackson2HttpMessageConverter is automatically chosen to convert
the Greeting instance to JSON.
You have to use something like this to automatically convert your request body json to a Java Object.
#RequestMapping("/getCar", method =RequestMethod.POST,
produces=MediaType.APPLICATION_JSON_VALUE,consumes=MediaType.APPLICATION_JSON_VALUE)
public Car getCar(#RequestBody Car car){
return car;
}
However, Spring will use an HttpMessageConverter at the backend to convert your json request to a POJO. Something like this should be added to your #Configuration class
public void configureMessageConverters() {
List<HttpMessageConverter<?> messageConverters = new ArrayList<>();
messageConverters.add(new MappingJackson2HttpMessageConverter());
super.configureMessageConverters(converters);
}
I make application in Java Spring similar to:
Java Blog Aggregator
Author of this app use Spring MVC and common http request. Is it fast way to remodel of controllers to full rest application which use AJAX?
I do not know where to start.
I would like to send JSON. I don't have my own code now, cause I just getting started.
Example of controller:
package cz.jiripinkas.jba.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import cz.jiripinkas.jba.service.ItemService;
#Controller
public class IndexController {
#Autowired
private ItemService itemService;
#RequestMapping("/index")
public String index(Model model) {
model.addAttribute("items", itemService.getItems());
return "index";
}
}
Thanks for any help.
Yes, you can do that, follow these steps:-
Download a JSON API jar and include that library in your application. You can download one HERE
Convert your public String index(Model model) method to public void index(Model model, HttpServletResponse response).
Remove the line return "index"; as now your method is of void type.
Contruct a JSON with the library you just added. You will finally have an object of type JSONObject
Once you have this object, convert and store into a String. For example, if you have a variable named returnJSONObj of type JSONObject, you can write convert it into a String as String returnJSONStr = returnJSONObj.toString()
Finally, you have write the output at a JSON String:-
response.setContentType("application/json");
response.getWriter.println(returnJSONStr);
This will make your method return a JSON String, which you can use in your REST API.
EDIT
Added sample code for reference
package cz.jiripinkas.jba.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import cz.jiripinkas.jba.service.ItemService;
#Controller
public class IndexController {
#Autowired
private ItemService itemService;
#RequestMapping("/index")
public void index(Model model, HttpServletResponse response) {
List items = itemService.getItems();
String returnJSONStr = createJSONStr(items);
response.setContentType("application/json");
response.getWriter().println(returnJSONStr); //this method throws IOException
}
private String createJSONStr(List items) {
JSONObject json = new JSONObject();
for(Item item: items) { //For each item
json.put("property1", item.getProperty1()); //replace by your own properties
json.put("property2", item.getProperty2());
}
return json.toString();
}
}
You can make this a REST Webservice by :
1) Replacing #Controller with #RestController.
2) Changing #RequestMapping("/index") to :
#RequestMapping(value="/index",
method=RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
3) And if you intent to on receiving some payload in this index web service then replace RequestMethod.GET with RequestMethod.POST and change parameters of index function to some DTO object which will map with json you want to receive and annotate it with #RequestBody(This is optional in case you are using #RestController).
EDIT :
4) If you want to return a JSON object , you just need to create a bean with attribute names same as your keys :
class User {
private String id;
private String name;
public void setName(String name){
this.name=name;
}
public void getName(){
return this.name;
}
public void getId(){
return this.id;
}
public void setId(String id){
this.id=id;
}
}
and return this object from the method as follows :
#RequestMapping(value="/index",
method=RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
public User index() {
User user = new User();
user.setId("1");
user.setName("Test");
return user;
}
I am using jersey 1.9.1. I have rest method like following where
Authorization header contained encoded credentials such as username
and password and it is parsed in a method and mapped local values.
#PUT
#Path(SystemConstants.REST_MESSAGE_SENDSMS)
#Consumes(MediaType.APPLICATION_JSON)
#Produces({MediaType.APPLICATION_JSON})
public Response sendSms(#HeaderParam("Authorization") String authorization, String param) {
String[] credentials = ImosUtils.getUserCredentials(authorization);
String username = credentials[0];
String password = credentials[1];
}
I am trying to design a way to make this process automatically, without writing same parsing code in each method. I mean I would like to know if writing a special annotation such as HeaderParamExtended to this is used to parse this credentials.
I am using jersey 1.9.1 version as rest api. Where I have to edit a class in that life cycle?
#PUT
#Path(SystemConstants.REST_MESSAGE_SENDSMS)
#Consumes(MediaType.APPLICATION_JSON)
#Produces({MediaType.APPLICATION_JSON})
public Response sendSms(#HeaderParamExtended("Authorization","username") String username, #HeaderParamExtended("Authorization","password") String password, , String param) {
}
Normally you need an InjectableProvider to support the custom injection, and also an Injectable to provide the value.
Here's an example
#BasicAuth
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface BasicAuth {
}
InjectableProvider
#Provider
public class BasicAuthInjectionProvider
implements InjectableProvider<BasicAuth, Parameter> {
#Override
public ComponentScope getScope() {
return ComponentScope.PerRequest;
}
#Override
public Injectable getInjectable(ComponentContext cc, BasicAuth a, Parameter c) {
return new BasicAuthInjectable();
}
}
Injectable
public class BasicAuthInjectable extends AbstractHttpContextInjectable<User>{
#Override
public User getValue(HttpContext hc) {
String authHeaderValue = hc.getRequest()
.getHeaderValue(HttpHeaders.AUTHORIZATION);
String[] credentials = ImosUtils.getUserCredentials(authHeaderValue);
return new User(credentials[0], credentials[1]);
}
}
One thing you'll notice is that I have a User class. This is to wrap the username and password, and just have one injection point. i.e.
public Response getSomething(#BasicAuth User user) {
}
I actually tried to do it your way, with
public Response getSomething(#BasicAuth("username") String username,
#BasicAuth("password") String password) {
And in the InjectableProvider get the annotation value from the annotation passed to the getInjectable, then pass that value onto the BasicAuthInjectable. From there check to see if the value is "username" or "password" and return the corresponding value. But for some reason the injection providers were not even called. You can play around with it to see if you can get it to work. But to me the User looks cleaner anyway, and with the two strings, the injection providers are called twice and you need to parse the headers twice. Seems unnecessary.