Is it possible to pass custom object into SpringBoot's endpoint? - java

I have a SpringBoot project and in an every endpoint's the first call is parsing a request header into a POJO object. making sure required headers are set. Is it possible "to teach" SpringBoot to be able to provide custom object in endpoints?
class CommonHeader {
private String callerId;
private String systemId;
....
public static CommonHeader parseCommonHeader(Map<String, String> map) {
CommonHeader header = new CommonHeader();
header.setCallerId(map.get("x-caller-id"));
.....
return header;
}
}
#Path("/columnConfigs")
Response getColumnConfig(#RequestHeader Map<String, String> headers) {
CommonHeader commonHeader = parseCommonHeader(headers);
....
}
#Path("/other")
Response getColumnConfig(#RequestHeader Map<String, String> headers) {
CommonHeader commonHeader = parseCommonHeader(headers);
....
}
I would like to be able to simplify the following code into:
#Path("/columnConfigs")
Response getColumnConfig(CommonHeader commonHeader) {
....
}
#Path("/other")
Response getColumnConfig(CommonHeader commonHeader) {
....
}

I was looking for some solution for this, that is, bind headers to an object of our definition. I found a good technique here: Using Custom Data Binders in SpringMVC
What you have to do in a nutshell are these, apart from the already created CommonHeader class:
Define an annotation, say #MyHeaders (with #Retention( RUNTIME ) and #Target( PARAMETER ))
#Retention( RUNTIME )
#Target( PARAMETER )
public #interface MyHeaders {}
Create an implementation of HandlerMethodArgumentResolver, say MyHeadersResolver. This will map the headers from the request into an instance of CommonHeader and return it.
#Override
public boolean supportsParameter( MethodParameter methodParameter ){
return methodParameter.getParameterAnnotation( MyHeaders.class ) != null;
}
#Override
public Object resolveArgument( MethodParameter methodParameter, ModelAndViewContainer mavc, NativeWebRequest req, WebDataBinderFactory wdbf ) throws Exception{
HttpServletRequest request = (HttpServletRequest) req.getNativeRequest();
CommonHeader h = new CommonHeader();
Collections.list( request.getHeaderNames() ).forEach( header -> {
switch( header.toLowerCase() ) {
//Set values into CommonHeader instance
}
});
return h;
}
Declare this to SpringMVC as an argument resolver in a class that implements WebMvcConfigurer, like this:
#Override
public void addArgumentResolvers( List<HandlerMethodArgumentResolver> resolvers ) {
resolvers.add( new MyHeadersResolver() );
}
Your controller method signature will now look like this:
Response getColumnConfig( #MyHeaders CommonHeader commonHeader )

Define CommonHeader to extend Map<String, String>:
#RequestHeader
public interface CommonHeader extends Map<String, String> {
}

Related

Convert #RequestParam to custom Object

Have a problem with optimizing search request.
I have search method that accepts parameters in url query like:
http://localhost:8080/api?code.<type>=<value>&name=Test
Example: http://localhost:8080/api?code.phone=9999999999&name=Test
Defined SearchDto:
public class SearchDto {
String name;
List<Code> code;
}
Defined Code class:
public class Code {
String type;
String value;
}
Currently I'm using Map<String,String> as incoming parameter for the method:
#GetMapping("/search")
public ResponseEntity<?> search(final #RequestParam Map<String, String> searchParams) {
return service.search(searchParams);
}
Then manually converting map values for SearchDto class. Is it possible to get rid of Map<String,String> and pass SearchDto directly as argument in controller method?
Passing a json in querystring is actually a bad practice, since it decrease the security and sets limits on the number of parameters you can send to your endpoint.
Technically speaking, you could make everything work by using your DTO as a controller's parameter, then URL encoding the json before you send it to the backend.
The best option, in your case, is to serve an endpoint that listen to a POST request: it is not an error, neither a bad practise, to use POST when performing a search.
you can customize a HandlerMethodArgumentResolver to implement it.
but , if you want a object receive incoming parameter. why not use POST
#Target({ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Example {
}
public class ExampleArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
Example requestParam = parameter.getParameterAnnotation(Example.class);
return requestParam != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, #Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, #Nullable WebDataBinderFactory binderFactory) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
Map<String, String[]> parameterMap = webRequest.getParameterMap();
Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
//here will return a map object. then you convert map to your object, I don't know how to convert , but you have achieve it.
return o;
}
}
add to container
#Configuration
#EnableWebMvc
public class ExampleMvcConfiguration implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new ExampleArgumentResolver());
}
}
usage
#RestController
public class TestCtrl {
#GetMapping("api")
public Object gg(#Example SearchDto searchDto) {
System.out.println(searchDto);
return "1";
}
#Data
public static class SearchDto {
String name;
List<Code> code;
}
#Data
public static class Code {
String type;
String value;
}
}
Here is a demo.

Spring ArgumentResolver to get pathvariable map and

this my method signature
#RequestMapping(value = {"/article", "/article/{id}", "/article/{name}"}, method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<JsonNode> get(#PathVariable Map<String, String> pathVarsMap, #RequestParam(value="test") MultiValueMap<String, String> test, #RequestBody(required=false) JsonNode requestBody )
I want to make this into
public ResponseEntity<JsonNode> get( MyStructure mystr)
where MyStructure will have #PathVariable Map<String, String> pathVarsMap, #RequestParam(value="test") MultiValueMap<String, String> test, #RequestBody(required=false) JsonNode requestBody inside of it.
I know that I have to use custom resolvers and implement resolveArgument. One of the examples i saw did (Map<String, String>) httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE). But im not sure how to get it to work. Can i create MultiValueMap and RequestBody inside MyString ?
In another place, I see that the recommendation is to use
#Nonnull
protected final Map<String, String> getUriTemplateVariables(NativeWebRequest request) {
#SuppressWarnings("unchecked")
Map<String, String> variables =
(Map<String, String>) request.getAttribute(
URI_TEMPLATE_VARIABLES_ATTRIBUTE, SCOPE_REQUEST);
return (variables != null) ? variables : Collections.<String, String>emptyMap();
}
so im a bit confused on how should i be implementing this
All #PathVariable , #RequestParam and #RequestBody can only be annotated on the method parameters , so there are no ways for you to annotate them on the object fields.
The codes of the existing HandlerMethodArgumentResolver that resolve the values for these annotations also assume these annotation are annotated on the method parameters ,that means you also cannot simply delegate to them to resolve the value for your request object.
Your best bet is to simply reference the corresponding HandlerMethodArgumentResolver for each annotation and copy the related codes to your implementation.
For #PathVariable , it is resolved by PathVariableMapMethodArgumentResolver
For #RequestParam on MultiValueMap , it is resolved by RequestParamMapMethodArgumentResolver
For #RequestBody , it is resolved by RequestResponseBodyMethodProcessor . Internally , it works with a list of HttpMessageConverter to read the HTTP request body. As you are now using Jackson to read the request body , you only need to focus on MappingJackson2HttpMessageConverter for simplicity.
It is easier than I expected. There following implementation should be a good starting point for you.
First define MyStructure class :
public class MyStructure {
public Map<String, String> pathVariables;
public MultiValueMap<String, String> queryParameters;
public JsonNode requestBody;
}
And implement MyStructureArgumentResolver :
public class MyStructureArgumentResolver implements HandlerMethodArgumentResolver {
private MappingJackson2HttpMessageConverter messageConverter;
public MyStructureArgumentResolver(MappingJackson2HttpMessageConverter messageConverter) {
super();
this.messageConverter = messageConverter;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return MyStructure.class.isAssignableFrom(parameter.getParameterType());
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
MyStructure request = new MyStructure();
request.queryParameters = resolveQueryParameters(webRequest);
request.pathVariables = resolvePathVariables(webRequest);
request.requestBody = resolveRequestBody(webRequest, parameter);
return request;
}
private MultiValueMap<String, String> resolveQueryParameters(NativeWebRequest webRequest) {
// resolve all query parameter into MultiValueMap
Map<String, String[]> parameterMap = webRequest.getParameterMap();
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
result.add(key, value);
}
});
return result;
}
private Map<String, String> resolvePathVariables(NativeWebRequest webRequest) {
Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (!CollectionUtils.isEmpty(uriTemplateVars)) {
return new LinkedHashMap<>(uriTemplateVars);
} else {
return Collections.emptyMap();
}
}
private JsonNode resolveRequestBody(NativeWebRequest webRequest, MethodParameter parameter)
throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest);
MediaType contentType;
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = parameter.getContainingClass();
JsonNode body = JsonNodeFactory.instance.objectNode();
if (messageConverter.canRead(JsonNode.class, contextClass, contentType)) {
body = (JsonNode) messageConverter.read(JsonNode.class, inputMessage);
}
return body;
}
}
Then register MyStructureArgumentResolver :
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Autowired
private MappingJackson2HttpMessageConverter messageConverter;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MyStructureArgumentResolver(messageConverter));
}
}
And use it in the controller method :
#RequestMapping(value = { "/test/{name}" }, method = RequestMethod.GET)
public ResponseEntity<String> test(MyStructure request) {
}
#PostMapping("/get")
public ResponseEntity<JsonNode> get( #RequestBody MyStructure mystr){...}
When call this api, fill in params in request body, send body as application/json. Refer to this sample: sample project

How to get a request path variable within a Filter? [Micronaut 1.3.2]

Usually in Spring we can retrieve the path variable with:
final Map<String, String> pathVariables = (Map<String, String>) request
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
Here is what I do so far.
Controller:
#Get(value = "/{variable}/anotherpath")
public Single<HttpResponse<ResponseESQ>> myController(String variable) {}
Filter :
#Filter("/**")
public class myFilter implements HttpServerFilter {
#Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
// I need here to consume the path variable
request.getAttribute("variable")
}
}
I try to use : request.getAttributes() but it doesn't work.
How can we do the same in Micronaut?
Given the following controller with an URI that contains two path variables something and name.
#Controller("/say")
public class SuperController {
#Get("{something}/to/{name}")
#Produces
public String hello(String something, String name) {
return String.format("%s %s", something, name);
}
}
You can write a filter that can access the path variables by accessing the io.micronaut.web.router.UriRouteMatch that is contained in io.micronaut.http.HttpMessage#getAttributes.
The following example filter accesses the path variables.
#Filter("/**")
public class SuperFilter implements HttpFilter {
#Override
public Publisher<? extends HttpResponse<?>> doFilter(HttpRequest<?> request, FilterChain chain) {
Optional<UriRouteMatch> uriRouteMatch = request
.getAttributes()
.get(HttpAttributes.ROUTE_MATCH.toString(), UriRouteMatch.class);
if (uriRouteMatch.isPresent()) {
// access the path variables.
Map<String, Object> variableValues = uriRouteMatch.get().getVariableValues();
System.out.println(variableValues);
}
return chain.proceed(request);
}
}
Hope this answers your question. Good luck and have fun with Micronaut.

How to add HTTP headers in Microprofile REST client dynamically?

I am developing application, which uses microprofile rest client. And that rest client should send REST request with various http header. Some headers names changes dynamically. My microprofile rest client should be generic, but I did not find how to implement such behaviour.
According to the documentation you need to specify all header names in the implementation via annotations and that is not generic. Is there any way how to "hack" it and add HTTP headers programatically?
Thanks in advance
GenericRestClient genericRestClient = null;
Map<String, Object> appConfig = context.appConfigs();
String baseUrl = (String) appConfig.get("restClient.baseUrl");
path = (String) appConfig.get("restClient.path");
try {
genericRestClient = RestClientBuilder.newBuilder()
.baseUri(new URI(baseUrl)).build(GenericRestClient.class);
}catch(URISyntaxException e){
logger.error("",e);
throw e;
}
Response response = genericRestClient.sendMessage(path, value);
logger.info("Status: "+response.getStatus());
logger.info("Response body: "+response.getEntity().toString());
Generic rest client code:
#RegisterRestClient
public interface GenericRestClient {
#POST
#Path("{path}")
#Produces("application/json")
#Consumes("application/json")
public Response sendMessage(<here should go any map of custom headers>, #PathParam("path") String pathParam, String jsonBody);
}
According to the spec, you can use a ClientHeadersFactory. Something like this:
public class CustomClientHeadersFactory implements ClientHeadersFactory {
#Override public MultivaluedMap<String, String> update(
MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders
) {
MultivaluedMap<String, String> returnVal = new MultivaluedHashMap<>();
returnVal.putAll(clientOutgoingHeaders);
returnVal.putSingle("MyHeader", "generated");
return returnVal;
}
}
#RegisterRestClient
#RegisterClientHeaders(CustomClientHeadersFactory.class)
public interface GenericRestClient {
...
}
You can't pass values directly to the ClientHeadersFactory; but you can directly access the headers of an incoming request, if your own service is called via JAX-RS. You can also #Inject anything you need. If this is still not sufficient and you really need to pass things from the service call, you can use a custom #RequestScope bean, e.g.:
#RequestScope
class CustomHeader {
private String name;
private String value;
// getters/setters
}
public class CustomClientHeadersFactory implements ClientHeadersFactory {
#Inject CustomHeader customHeader;
#Override public MultivaluedMap<String, String> update(
MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders
) {
MultivaluedMap<String, String> returnVal = new MultivaluedHashMap<>();
returnVal.putAll(clientOutgoingHeaders);
returnVal.putSingle(customHeader.getName(), customHeader.getValue());
return returnVal;
}
}
class Client {
#Inject CustomHeader customHeader;
void call() {
customHeader.setName("MyHeader");
customHeader.setValue("generated");
...
Response response = genericRestClient.sendMessage(path, value);
}
}
Add ClientRequestHeader to your client as follows:
#POST
// read from application.properties
#ClientRequestHeader(name=“myHeader1”, value="${myProperty}")
// read from a method
#ClientRequestHeader(name=“myHeader2”, value="{addHeaderMethod}")
Response sendMessage();
default String addHeaderMethod() {
//put your logic here
return “my dynamic value”;
}

Map as parameter in RestAPI Post request

I have created an API with a Map<String, Integer> parameter, like this:
#RequestMapping(value = "upload", method = RequestMethod.POST)
public ResponseEntity<String> handleContactsFileUpload(#RequestParam("file") MultipartFile file,
#RequestParam("name") String name,
#RequestParam("campaignAppItemId") Long campaignAppItemId,
#RequestParam("fileColumnHeaders") Map<String,Integer> fileColumnHeaders) throws Exception {
if (file == null)
return new ResponseEntity<>("No file uploaded", HttpStatus.BAD_REQUEST);
contactService.handleContactsFile(file, name, campaignAppItemId,fileColumnHeaders);
return new ResponseEntity<>("File uploaded successfully", HttpStatus.OK);
}
I am trying to call this via Postman:
I passed the fileColumnHeaders inside Body->Form Data as in the screenshot.
Then I got a message like this in Postman:
Failed to convert value of type 'java.lang.String' to required type 'java.util.Map'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found.
Anybody know why this message came ?
How can we pass a map as a parameter in Rest API request?
How can we pass a map through Postman?
You could use #RequestBody instead of #RequestParam for Maps and other non trivial data types and objects - this way spring will map the JSON representing your map parameter to a domain object, which is then serializable and can be converted to a java object.
... Or simply create a converter:
#Component
#RequiredArgsConstructor
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
private final ObjectMapper objectMapper;
#Override
public Map<String, Object> convert(String source) {
try {
return objectMapper.readValue(source, new TypeReference<Map<String, String>>() {
});
} catch (final IOException e) {
return null;
}
}
}
Firstly, you create DTO object to get all data from your request.
public class FormDataDTO {
private MultipartFile file;
private String name;
private Long campaignAppItemId;
private Map<String,Integer> fileColumnHeaders;
// getters, setters
}
Secondly, you can map FormDataDTO from your request without any annotation:
#RequestMapping(value = "upload", method = RequestMethod.POST)
public ResponseEntity<String> handleContactsFileUpload(FormDataDTO formDataDTO){
// your logic code here
}
Finally, form-data in your request will be:
I think this could work:
#RequestMapping(value = "upload/{fileColumnHeaders}", method = RequestMethod.POST)
public ResponseEntity<String> handleContactsFileUpload(#RequestParam("file") MultipartFile file,
#RequestParam("name") String name,
#RequestParam("campaignAppItemId") Long campaignAppItemId,
#MatrixVariable Map<String,Integer> fileColumnHeaders) throws Exception {
if (file == null)
return new ResponseEntity<>("No file uploaded", HttpStatus.BAD_REQUEST);
contactService.handleContactsFile(file, name, campaignAppItemId,fileColumnHeaders);
return new ResponseEntity<>("File uploaded successfully", HttpStatus.OK);
}
Put all other parameters into the body, but add the fileColumnHeaders to the URL like this:
/upload/firstName=1;lastName=2;address=3;phone=4
You will also need this extra configuration:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}

Categories

Resources