Spring custom annoatation use response body directly inside annotation - java

I am writing custom log handler which will log events and will be used on top of controller methods like below,
#RecordEvent(authType = "EVENT", response = "", inputParams = "111")
#GetMapping(value = "getRoles", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> getRoles(Authentication a, HttpServletRequest req,String test) {
test="ahdkjhasdkjhaskd";
................................
.................................
Map<String, Object> map = new HashMap<>();
return map;
}
package trinity.sso.logging;
import java.lang.annotation.*;
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface RecordEvent {
String authType() ;
String inputParams() ;
String response() ;
}
#Aspect
#Component
public class EventLoggingComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(EventLoggingComponent.class);
private ExpressionParser expressionParser = new SpelExpressionParser();
private TemplateParserContext parserContext = new TemplateParserContext();
#Pointcut("#annotation(recordEvent)")
public void executeRecordEvent(RecordEvent recordEvent) {
}
#AfterReturning(pointcut = "executeRecordEvent(recordEvent)", returning = "result")
public void logEvent(JoinPoint joinPoint, RecordEvent recordEvent, Object result) {
//log here
}
}
So I wanted to make the response param in annotation as user defined where he can use combintation of response object template to form a string. Like response.get("status")+" is "+response.get("description"). But how user can access direct response body like this or is there any alterntive to form string like this using response body and status code. it will be different for each method. So, user can define custom message using response body and status for each method.
Is there any method or alternative to achieve this?

Related

How can I get api with FeignClient

I used Lombok, Open Feign and Spring Web
I have currencyClient interface:
#FeignClient(value = "getcurrency", url = "https://openexchangerates.org")
public interface currencyClient {
#RequestMapping(value = "/api/historical/2012-07-10.json/{smt}", method = RequestMethod.GET)
public List<Object> getCurrency(#PathVariable String smt);
}
And Controller:
#RestController
#RequiredArgsConstructor
public class StatusController {
private String appId1 = "appId";
private final currencyClient currencyClient;
#GetMapping("/getAllCurrency")
public List<Object> getCurrency(){
return currencyClient.getCurrency(appId1);
}
}
And "http://localhost:1212/getAllCurrency" is not working cause the link is converted into "**https://openexchangerates.org/api/historical/2012-07-10.json/appId**" I understand that &/= are reversed and I also think that my indication of List is not correct. That's what I tried so how can I get info from "**https://openexchangerates.org/api/historical/2012-07-10.json?app_id**" as "http://localhost:1212/getAllCurrency"?
According to the https://docs.openexchangerates.org docs, the app_id should be a request parameter (see #RequestParam), not a path variable. You could do something like this:
CurrencyClient interface:
#FeignClient(value = "getcurrency", url = "https://openexchangerates.org")
public interface CurrencyClient {
#RequestMapping(value = "/api/historical/2012-07-10.json", method = RequestMethod.GET)
Map<String, Object> getCurrency(#RequestParam("app_id") String appId);
}
StatusController:
#RestController
public class StatusController {
private final CurrencyClient currencyClient;
public MyController(CurrencyClient currencyClient) {
this.currencyClient = currencyClient;
}
#GetMapping("/getAllCurrency")
public Map<String, Object> getCurrency() {
String appId1 = "*****";
return currencyClient.getCurrency(appId1);
}
}
Some additional things to note here:
Please don't post yout API key to StackOverflow, or anywhere else publicly. Other people might abuse it. Since you already posted it, you should request a new API key and get rid of this one (close it if possible).

How to write a feign client for an api that has multiple identically named query params?

I am trying to write a feign client to make calls to retrieve data from a server where the api accepts a list of identical named query parameters to determine how much data is being asked. Here is an example url I am trying to hit:
http://some-server/some-endpoint/{id}?include=profile&include=account&include=address&include=email
So far for my feign client I'm attempting to set it up this way:
#FeignClient("some-server")
public interface SomeServerClient {
#RequestMapping(method = RequestMethod.GET,
value = "/customers/api/customers/{id}",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
Map<Object, Object> queryById(
#PathVariable long id,
#RequestParam("include[]") String ... include);
default Map<Object, Object> queryById(long id) {
return queryById(id,"profile", "account", "address", "email");
}
However this doesn't appear t format the request in the way desired, so my question is how can I set up my feign client to submit its request to the url as shown in the example above?
use #RequestParam("include") List<String> includes, example:
client:
#FeignClient(value = "foo-client")
public interface FooClient {
#GetMapping("/foo")
Foo getFoo(#RequestParam("include") List<String> includes);
}
controller:
#RestController
public class FooController {
#GetMapping("/foo")
public Foo getFoo(#RequestParam("include") List<String> includes) {
return new Foo(includes);
}
}
usage:
List<String> includes = new ArrayList<>();
includes.add("foo");
includes.add("bar");
Foo foo = fooClient.getFoo(includes);
url:
http://some-server/foo?include=foo&include=bar

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);
}
}

Custom error message containing parameter names when validation fails

I would like my API to return errorMessage when the request lacks of required parameters. For example let's say there is a method:
#GET
#Path("/{foo}")
public Response doSth(#PathParam("foo") String foo, #NotNull #QueryParam("bar") String bar, #NotNull #QueryParam("baz") String baz)
where #NotNull is from package javax.validation.constraints.
I wrote an exception mapper which looks like this:
#Provider
public class Mapper extends ExceptionMapper<ConstraintViolationException> {
#Override
public Response toResponse(ConstraintViolationException) {
Iterator<ConstraintViolation<?>> it= exception.getConstraintViolations().iterator();
StringBuilder sb = new StringBuilder();
while(it.hasNext()) {
ConstraintViolation<?> next = it.next();
sb.append(next.getPropertyPath().toString()).append(" is null");
}
// create errorMessage entity and return it with apropriate status
}
but next.getPropertyPath().toString() returns string in format method_name.arg_no, f.e. fooBar.arg1 is null
I'd like to receive output fooBar.baz is null or simply baz is null.
My solution was to include -parameters parameter for javac but to no avail.
Probably I could somehow achieve it with the use of filters:
public class Filter implements ContainerResponseFilter {
#Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
UriInfo uriInfo = requestContext.getUriInfo();
UriRoutingContext routingContext = (UriRoutingContext) uriInfo;
Throwable mappedThrowable = routingContext.getMappedThrowable();
if (mappedThrowable != null) {
Method resourceMethod = routingContext.getResourceMethod();
Parameter[] parameters = resourceMethod.getParameters();
// somehow transfer these parameters to exceptionMapper (?)
}
}
}
The only problem with the above idea is that ExeptionMapper is executed first, then the filter is executed. Also I have no idea how could I possibly transfer errorMessage between ExceptionMapper and Filter. Maybe there is another way?
You can inject ResourceInfo into the exception mapper to get the resource method.
#Provider
public class Mapper extends ExceptionMapper<ConstraintViolationException> {
#Context
private ResourceInfo resourceInfo;
#Override
public Response toResponse(ConstraintViolationException ex) {
Method resourceMethod = resourceInfo.getResourceMethod();
Parameter[] parameters = resourceMethod.getParameters();
}
}

Spring Boot using Json as request parameters instead of an entity/model

Our company is planning to switch our microservice technology to Spring Boot. As an initiative I did some advanced reading and noting down its potential impact and syntax equivalents. I also started porting the smallest service we had as a side project.
One issue that blocked my progress was trying to convert our Json request/response exchange to Spring Boot.
Here's an example of the code: (This is Nutz framework for those who don't recognize this)
#POST
#At // These two lines are equivalent to #PostMapping("/create")
#AdaptBy(type=JsonAdapter.class)
public Object create(#Param("param_1") String param1, #Param("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
On PostMan or any other REST client I simply pass POST:
{
"param_1" : "test",
"param_2" : 1
}
I got as far as doing this in Spring Boot:
#PostMapping("/create")
public Object create(#RequestParam("param_1") String param1, #RequestParam("param_2) int param2) {
MyModel1 myModel1 = new MyModel1(param1);
MyModel2 myModel2 = new MyModel2(param2);
myRepository1.create(myMode12);
myRepository2.create(myModel2);
return new MyJsonResponse();
}
I am not sure how to do something similar as JsonAdapter here. Spring doesn't recognize the data I passed.
I tried this but based on the examples it expects the Json paramters to be of an Entity's form.
#RequestMapping(path="/wallet", consumes="application/json", produces="application/json")
But I only got it to work if I do something like this:
public Object (#RequestBody MyModel1 model1) {}
My issue with this is that MyModel1 may not necessarily contain the fields/parameters that my json data has.
The very useful thing about Nutz is that if I removed JsonAdapter it behaves like a regular form request endpoint in spring.
I couldn't find an answer here in Stack or if possible I'm calling it differently than what existing spring devs call it.
Our bosses expect us (unrealistically) to implement these changes without forcing front-end developers to adjust to these changes. (Autonomy and all that jazz). If this is unavoidable what would be the sensible explanation for this?
In that case you can use Map class to read input json, like
#PostMapping("/create")
public Object create(#RequestBody Map<String, ?> input) {
sout(input.get("param1")) // cast to String, int, ..
}
I actually figured out a more straightforward solution.
Apparently this works:
#PostMapping("/endpoint")
public Object endpoint(#RequestBody MyWebRequestObject request) {
String value1 = request.getValue_1();
String value2 = request.getValue_2();
}
The json payload is this:
{
"value_1" : "hello",
"value_2" : "world"
}
This works if MyRequestObject is mapped like the json request object like so. Example:
public class MyWebRequestObject {
String value_1;
String value_2
}
Unmapped values are ignored. Spring is smart like that.
I know this is right back where I started but since we introduced a service layer for the rest control to interact with, it made sense to create our own request model object (DTOs) that is separate from the persistence model.
You can use #RequestBody Map as a parameter for #PostMapping, #PutMapping and #PatchMapping. For #GetMapping and #DeleteMapping, you can write a class which implements Converter to convert from json-formed request parameters to Map. And you would register that class as a bean with #Component annotation. Then you can bind your parameters to #RequestParameter Map.
Here is an example of Converter below.
#Component
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
private final ObjectMapper objectMapper;
#Autowired
public StringToMapConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public Map<String, Object> convert(String source) {
try {
return objectMapper.readValue(source, new TypeReference<Map<String, Object>>(){});
} catch (IOException e) {
return new HashMap<>();
}
}
}
If you want to exclude specific field of your MyModel1 class, use #JsonIgnore annotation onto the field like below.
class MyModel1 {
private field1;
#JsonIgnore field2;
}
Then, I guess you can just use what you have done.(I'm not sure.)
public Object (#RequestBody MyModel1 model1) {}
i think that you can use a strategy that involve dto
https://auth0.com/blog/automatically-mapping-dto-to-entity-on-spring-boot-apis/
you send a json to your rest api that is map like a dto object, after you can map like an entity or use it for your needs
try this:
Add new annotation JsonParam and implement HandlerMethodArgumentResolver of this, Parse json to map and get data in HandlerMethodArgumentResolver
{
"aaabbcc": "aaa"
}
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonParam {
String value();
}
#Component
public class JsonParamMethodResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonParam.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
RepeatedlyRequestWrapper nativeRequest = webRequest.getNativeRequest(RepeatedlyRequestWrapper.class);
if (nativeRequest == null) {
return null;
}
Gson gson = new Gson();
Map<String, Object> response = gson.fromJson(nativeRequest.getReader(), new TypeToken<Map<String, Object>>() {
}.getType());
if (response == null) {
return null;
}
JsonParam parameterAnnotation = parameter.getParameterAnnotation(JsonParam.class);
String value = parameterAnnotation.value();
Class<?> parameterType = parameter.getParameterType();
return response.get(value);
}
}
#Configuration
public class JsonParamConfig extends WebMvcConfigurerAdapter {
#Autowired
JsonParamMethodResolver jsonParamMethodResolver;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(jsonParamMethodResolver);
}
}
#PostMapping("/methodName")
public void methodName(#JsonParam("aaabbcc") String ddeeff) {
System.out.println(username);
}

Categories

Resources