How to utilize specific argument in spring validator and I cannot merge arguments into a single object?
Say I have a controller like,
#RequestMapping(value = "url",
consumes = { "application/json" }, produces = { "application/json" },
method = RequestMethod.POST)
public MyResponse getMyData(
#Valid #RequestBody MyRequest myRequest, Errors errors, CustomObject obj){
return new MyResponse();
}
I have a aspect implementation for updating the CustomObject
#Around("#annotation(MyAnnotation) && execution(* MyController.getMyData(..))")
public Object requestsAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
Object value = null;
CustomObject obj = new CustomObject();
try {
Object[] args = proceedingJoinPoint.getArgs();
//populate CustomObject
obj = (CustomObject)args[2];
obj.setField("value");
value = proceedingJoinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
}
return value;
}
CustomObject's updated value I'm able to get in the controller. Now i need to validate MyRequest also using a spring custom validator like,
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new MyCustomValidator());
}
And MyCustomValidator's 'validate()' function is as,
#Override
public void validate(Object target, Errors errors) {
validate((MyRequest)target);
}
Both aspect and validation are working separately but when both are integrated the validation is failing because target in validate method considers 'CustomObject' for validation rather than using 'MyRequest'. Validate method will accept only two arguments while 'getMyData()' method in controller has three. How to resolve this or is there any other approach to achieve a similar solution?
You can try to assign the name of the object to validate to the initBinder (use the name of the object instance, not the class).
InitBinder("myRequest")
protected void initBinder(WebDataBinder binder) {
...
If it's not working, you can try to give an order to the aspect using the Order annotation on it or implement the Order interface.
Anyway, I'm not totally confident about the correctness of the first two options because I cannot test the code (I answer from a tablet). So, I suggest also a third option, where you put the validation inside the controller
#RequestMapping(value = "url",
consumes = { "application/json" },
produces = {"application/json" },
method = RequestMethod.POST)
public MyResponse getMyData(
#RequestBody MyRequest myRequest, BindingResult result, CustomObject obj){
MyCustomValidator validator = MyCustomValidator();
validator.validate(myRequest, result);
if (result.hasErrors()){
// do something
}
return new MyResponse();
}
Related
I have a post request with URL http://my-custom-url/{param}
now
//pseudo code
if param == param1
then request body must be mapped to model Class A
if param == param2
then request body must be mapped to model Class B
In addition to mapping I also have Jsr303 validation inside my model class es hence I also want to use #Valid annotation
How do I do this
You can receive the payload as a Map<K, V> and use ObjectMapper to deserialize it.
#RestController
#RequestMapping(path = "/my-rest-api")
public class MyController {
#Autowired
ClassAValidator classAValidator; // assuming you wrote an validator
#Autowired
ClassBValidator classBValidator; // assuming you wrote an validator
#PostMapping("/{parameter}")
public ResponseEntity<Object> handleRequest(
#PathVariable("parameter") String parameter,
#RequestBody Map<Object, Object> request,
BindingResult result) {
ObjectMapper mapper = new ObjectMapper();
if (parameter.equals("X")) {
ClassA classA = mapper.convertValue(request, ClassA.class);
classAValidator.validate(classA, result);
if(result.hasErrors()) {
throw new CustomException(result);
}
} else if (parameter.equals("Y")) {
ClassB classB = mapper.convertValue(request, ClassB.class);
classBValidator.validate(classB, result);
if(result.hasErrors()) {
throw new CustomException(result);
}
} else {
// ..
}
return ResponseEntity.ok("Ok");
}
}
I think this one is a simpler solution:
#RestController
#RequestMapping("/my-custom-url")
public class MyController {
#PostMapping("/param1")
public ResponseEntity<Object> handleClassA(
#Valid #RequestBody ClassA classA,
BindingResult result) {
...
return ResponseEntity.ok("Ok");
}
#PostMapping("/param2")
public ResponseEntity<Object> handleClassB(
#Valid #RequestBody ClassB classB,
BindingResult result) {
...
return ResponseEntity.ok("Ok");
}
}
I am not sure why above answers explaining validators. as I understand you are trying to return two type of responses based on condition. that is not possible. because return type validate on compile time. so if you set return type as classA you must return classA. you cannot return classB. (unless classB is a classA relationship) so if you really want to do this you can use name interface to enforce the type.
#GetMapping(value = "/{id}")
public Response getRent(#PathVariable int id, #RequestParam(required = false) String type) {
if(type=="A"){
return new ClassA(rentService.findById(id));
}else{
return new ClassB (rentService.findDetailResponse(id));
}
}
P.S: you may need to return this as
public ResponseEntiry<Response>
but I just used Response directly.
now you need to create those two classes from same interface
public interface Response {
}
public class ClassA implements Response {
// your code goes here
}
public class ClassB implements Response {
// code here
}
this way you can achieve what you are trying.
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);
}
I'd like to create a single method and configure both GET + POST on it, using spring-mvc:
#RestController
public class MyServlet {
#RequestMapping(value = "test", method = {RequestMethod.GET, RequestMethod.POST})
public void test(#Valid MyReq req) {
//MyReq contains some params
}
}
Problem: with the code above, any POST request leads to an empty MyReq object.
If I change the method signature to #RequestBody #Valid MyReq req, then the post works, but the GET request fails.
So isn't is possible to just use get and post together on the same method, if a bean is used as input parameters?
The best solution to your problem seems to be something like this:
#RestController
public class MyServlet {
#RequestMapping(value = "test", method = {RequestMethod.GET})
public void testGet(#Valid #RequestParam("foo") String foo) {
doStuff(foo)
}
#RequestMapping(value = "test", method = {RequestMethod.POST})
public void testPost(#Valid #RequestBody MyReq req) {
doStuff(req.getFoo());
}
}
You can process the request data in different ways depending on how you receive it and call the same method to do the business logic.
#RequestMapping(value = "/test", method = { RequestMethod.POST, RequestMethod.GET })
public void test(#ModelAttribute("xxxx") POJO pojo) {
//your code
}
This will work for both POST and GET. (make sure the order first POST and then GET)
For GET your POJO has to contain the attribute which you're using in request parameter
like below
public class POJO {
private String parameter1;
private String parameter2;
//getters and setters
URl should be like below
/test?parameter1=blah
Like this way u can use it for both GET and POST
I was unable to get this working on the same method and I'd like to know a solution, but this is my workaround, which differs from luizfzs's in that you take the same request object and not use #RequestParam
#RestController
public class Controller {
#GetMapping("people")
public void getPeople(MyReq req) {
//do it...
}
#PostMapping("people")
public void getPeoplePost(#RequestBody MyReq req) {
getPeople(req);
}
}
I have
#RequestMapping(method = RequestMethod.GET)
#ResponseBody
SessionInfo register(UserProfile profileJson){
...
}
I pass profileJson this way:
http://server/url?profileJson={"email": "mymail#gmail.com"}
but my profileJson object has all null fields. What should I do to make spring parse my json?
The solution to this is so easy and simple it will practically make you laugh, but before I even get to it, let me first emphasize that no self-respecting Java developer would ever, and I mean EVER work with JSON without utilizing the Jackson high-performance JSON library.
Jackson is not only a work horse and a defacto JSON library for Java developers, but it also provides a whole suite of API calls that makes JSON integration with Java a piece of cake (you can download Jackson at http://jackson.codehaus.org/).
Now for the answer. Assuming that you have a UserProfile pojo that looks something like this:
public class UserProfile {
private String email;
// etc...
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
// more getters and setters...
}
...then your Spring MVC method to convert a GET parameter name "profileJson" with JSON value of {"email": "mymail#gmail.com"} would look like this in your controller:
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper; // this is your lifesaver right here
//.. your controller class, blah blah blah
#RequestMapping(value="/register", method = RequestMethod.GET)
public SessionInfo register(#RequestParam("profileJson") String profileJson)
throws JsonMappingException, JsonParseException, IOException {
// now simply convert your JSON string into your UserProfile POJO
// using Jackson's ObjectMapper.readValue() method, whose first
// parameter your JSON parameter as String, and the second
// parameter is the POJO class.
UserProfile profile =
new ObjectMapper().readValue(profileJson, UserProfile.class);
System.out.println(profile.getEmail());
// rest of your code goes here.
}
Bam! You're done. I would encourage you to look through the bulk of Jackson API because, as I said, it is a lifesaver. For example, are you returning JSON from your controller at all? If so, all you need to do is include JSON in your lib, and return your POJO and Jackson will AUTOMATICALLY convert it into JSON. You can't get much easier than that. Cheers! :-)
This could be done with a custom editor, that converts the JSON into a UserProfile object:
public class UserProfileEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
ObjectMapper mapper = new ObjectMapper();
UserProfile value = null;
try {
value = new UserProfile();
JsonNode root = mapper.readTree(text);
value.setEmail(root.path("email").asText());
} catch (IOException e) {
// handle error
}
setValue(value);
}
}
This is for registering the editor in the controller class:
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(UserProfile.class, new UserProfileEditor());
}
And this is how to use the editor, to unmarshall the JSONP parameter:
#RequestMapping(value = "/jsonp", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
#ResponseBody
SessionInfo register(#RequestParam("profileJson") UserProfile profileJson){
...
}
You can create your own Converter and let Spring use it automatically where appropriate:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
#Component
class JsonToUserProfileConverter implements Converter<String, UserProfile> {
private final ObjectMapper jsonMapper = new ObjectMapper();
public UserProfile convert(String source) {
return jsonMapper.readValue(source, UserProfile.class);
}
}
As you can see in the following controller method nothing special is needed:
#GetMapping
#ResponseBody
public SessionInfo register(#RequestParam UserProfile userProfile) {
...
}
Spring picks up the converter automatically if you're using component scanning and annotate the converter class with #Component.
Learn more about Spring Converter and type conversions in Spring MVC.
This does solve my immediate issue, but I'm still curious as to how you might pass in multiple JSON objects via an AJAX call.
The best way to do this is to have a wrapper object that contains the two (or multiple) objects you want to pass. You then construct your JSON object as an array of the two objects i.e.
[
{
"name" : "object1",
"prop1" : "foo",
"prop2" : "bar"
},
{
"name" : "object2",
"prop1" : "hello",
"prop2" : "world"
}
]
Then in your controller method you recieve the request body as a single object and extract the two contained objects. i.e:
#RequestMapping(value="/handlePost", method = RequestMethod.POST, consumes = { "application/json" })
public void doPost(#RequestBody WrapperObject wrapperObj) {
Object obj1 = wrapperObj.getObj1;
Object obj2 = wrapperObj.getObj2;
//Do what you want with the objects...
}
The wrapper object would look something like...
public class WrapperObject {
private Object obj1;
private Object obj2;
public Object getObj1() {
return obj1;
}
public void setObj1(Object obj1) {
this.obj1 = obj1;
}
public Object getObj2() {
return obj2;
}
public void setObj2(Object obj2) {
this.obj2 = obj2;
}
}
Just add #RequestBody annotation before this param
I have controller:
#Controller
public class EventMenuController{
#RequestMapping(value = "/updateEvent", method = RequestMethod.POST)
public String updateEvent(Model model,
#Valid #ModelAttribute("existedEvent") Event event,
BindingResult result,
#ModelAttribute("linkedCandidates") Set<Candidate> candidates,
#ModelAttribute("linkedvacancies") Set<Vacancy> vacancies,
#RequestParam(required = true, value = "selectedEventStatusId")Integer EventStatusId,
#RequestParam(required = true, value = "selectedEventTypeId")Integer EventTypeId ,
RedirectAttributes attributes) {
if (result.hasErrors()) {
//model.addAttribute("idEvent", event.getId());
event.setCandidates(candidates);
event.setVacancies(vacancies);
return "eventDetails";
}
eventService.updateEventAndLinkedEntities(event, candidates, vacancies ,EventTypeId,EventStatusId);
attributes.addAttribute("idEvent",event.getId() );//event is null therefore NPE here
attributes.addAttribute("message", "submitted correctly at "+new Date());
return "redirect:eventDetails";
}
}
For testing this method I wrote following class:
#ContextConfiguration(locations = { "classpath:/test/BeanConfigUI.xml" })
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
public class EventMenuControllerTest {
#Test
public void updateEvent() throws Exception{
MockHttpServletRequestBuilder request = MockMvcRequestBuilders
.post("/updateEvent");
request.param("selectedEventStatusId", "1");
request.param("selectedEventTypeId", "1");
EventMenuController eventMenuController = (EventMenuController) wac.getBean("eventMenuController");
EventService mockEventService = Mockito.mock(EventService.class);
eventMenuController.eventService = mockEventService;
Mockito.doNothing().when(mockEventService).updateEventAndLinkedEntities(any(Event.class), any(Set.class),any(Set.class), any(Integer.class), any(Integer.class));
ResultActions result = mockMvc.perform(request);
result.andExpect(MockMvcResultMatchers.view().name("redirect:eventDetails"));
result.andExpect(MockMvcResultMatchers.model().attributeExists("idEvent"));
result.andExpect(MockMvcResultMatchers.model().attributeExists("message"));
}
}
In the process request executing on server side I see error that show me that event object is null.
Question:
What request I must write that pass event to server-side(controller method) using MockMvc ?
The Event class object is not initialized. You can either create an Event object or create a mock of an Event object depending on your test case and send it to the EventMenuController class object. You can do this similar to how you have sent a mock object of EventService to EventMenuController.
It is better practice to use fields as part of a class and not as part of a method. This will give you flexibility to mock any field.