I've RESTful service Spring MVC based.
The service has a RESTful resource method that returns the following response:
public class OperationalDataResponse<T> {
private String status;
private String statusMessage;
private T result;
//getters and setters
}
This response object encapsulates the result object of type T.
On the client side I use RestTemplate with GsonHttpMessageConverter added.
I get the response from service as a ResponseEntity
I handle the generic response with runtime Type as below:
public class OperationalDataRestClient<REQ,RESULT_TYPE> {
public OperationalDataResponse<RESULT_TYPE> getOperationalData(String resourcePath, Map<String, Object> urlVariables, Class<RESULT_TYPE> resultType) {
//code to invoke service and get data goes here
String responseString = responseEntity.getBody();
response = GsonHelper.getInstance().fromJson(responseString, getType(OperationalDataResponse.class, resultType));
}
Type getType(final Class<?> rawClass, final Class<?> parameter) {
return new ParameterizedType() {
#Override
public Type[] getActualTypeArguments() {
return new Type[] { parameter };
}
#Override
public Type getRawType() {
return rawClass;
}
#Override
public Type getOwnerType() {
return null;
}
};
}
}
This works like a charm as long as my resultType is a non-collection class.
So, this works great from caller code:
getOperationalData(someResourcePath, someUrlVariables, MyNonGenericClass.class)
However if my resultType is a collection (say, List<String> or List<MyNonGenericClass>)
then I don't know how to pass the resultType Class from the caller code.
For example, from caller code,
getOperationalData(someResourcePath, someUrlVariables, List.class)
or
getOperationalData(someResourcePath, someUrlVariables, List<MyNonGenericClass>.class)
throws compilation error.
I tried passing on ArrayList.class as well but that too doesn't work.
Any suggestion how can I pass a generic collection as a resultType from caller code (in other words, as an example, how can I pass the class object of a List<String> or List<MyNonGenericClass> from caller code ?)
If you know that ResultType is coming as a List, Then it will obvious fail like you said compilation issue.Why? because you are trying to send a List when you method only accepts a single value.In order to over come that issue you will have to change the method arguments to the following
public OperationalDataResponse<RESULT_TYPE> getOperationalData(String resourcePath, Map<String, Object> urlVariables, List<Class<RESULT_TYPE>> resultType){
....
}
and you will have to make some slight modification to getType() Method,loop it and then pass each class value to getType method like so
for(MyNonGenericClass myClass:mylist){
getType(OperationalDataResponse.class, myClass.getClass());
}
Related
I am very new to spring boot and I can't figure out the #Controller class. What should I pass if I can't find a particular object in my DB in spring boot? Is it better if I declare my return type as Response Entity and send a null User object?
//Get single user
#GetMapping("/users/{id}")
public User getUser(#PathVariable String id){
try {
Long i = Long.parseLong(id);
} catch (NumberFormatException ex) {
return ???? //Can't figure out what to return here.
}
return userService.getUser(id);
}
I want the consumer to know that they've sent an invalid string.
2) Also, User's variable id is of Long type. So should I take the argument as Long in the getUser function or take a String and parse it? Taking a Long would crash my server if a string was sent in the link.
This is my typical code of a REST controller for 'get user by id' request:
#RestController
#RequestMapping("/users") // 1
public class UserController {
private final UserRepo userRepo;
public UserController(UserRepo userRepo) {
this.userRepo = userRepo;
}
#GetMapping("/{id}") // 2
public ResponseEntity getById(#PathVariable("id") Long id) { // 3
return userRepo.findById(id) // 4
.map(UserResource::new) // 5
.map(ResponseEntity::ok) // 6
.orElse(ResponseEntity.notFound().build()); // 7
}
}
Where:
1 - is a common starting path for all requests handled by this controller
2 - a path variable pattern of GET request (/users/{id}).
3 - provide the name of path variable which name is correspond to the parameter in GetMapping. The type of the parameter in getById method is corresponds to the type of User ID.
4 - I use findById method of my UserRepo that return Optional
5 - Here I transform User to some type of DTO - UserResource (it's optional step)
6 - return OK response if User was found
7 - or return Not Found response otherwise.
I'm also using the controller-service-repository pattern in a couple of projects, and this is how I lay it out:
Controller.java
#RestController // 1
#RequestMapping(value = "/users") // 2
public class UserController {
private final UserService userService;
#Autowired // 3
public UserController(UserService userService) {
this.userService = userService;
}
#RequestMapping(value = "/{user_id}", method = RequestMethod.GET) //4
#ResponseStatus(HttpStatus.OK) //5
public UserModel getUser(#PathVariable(value="user_id") long user_id) { //6
return userService.getUserById(user_id);
}
#RequestMapping(method = RequestMethod.POST) // 7
#ResponseStatus(HttpStatus.CREATED) // 8
public UserModel getUser(#ResponseBody UserModel userModel) { // 9
return userService.createUser(usermodel);
}
}
1) #RestController is a combination of #Controller and #ResponseBody, which essentially means every method in your class will have a response body.
2) Prefix #RequestMapping values in this class with /users
3) Autowiring in the Constructor is the safest approach to injecting beans.
4) This method will be accessible via a GET request to /users/{user_id}
5) This method will return HttpStatus.OK status code on success (200)
6) Extracts the path variable "user_id" from the Request. Use the same numeric type as your user id's here (i.e. int or long).
7) This method will be accessible via a POST request to /users
8) This method will return HttpStatus.CREATED status code on success (201)
9) Extracts a UserModel from the request body (should have the same structure as the json given later).
There are no real differences to Cepr0 and my approach, it's purely a style preference.
UserModel can be a class like this:
UserModel.java
public class UserModel {
private String username;
// Constructor, Getter, Setter...
}
And this will return a JSON object in the body of the response like this:
{
"username":"username"
}
If you would like to handle Exceptions within your controller (and even control the data returned by an exception, you can use #ExceptionHandler like so:
#ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseWrapper> handleGenericException(Exception ex){
return ResponseEntity
.status(HttpStatus.I_AM_A_TEAPOT)
.body(new CustomExceptionWrapper(ex)));
}
Where CustomExceptionHandler converts exceptions thrown by your application into a format you decide (This can also be a POJO and Spring Boot will transform it into JSON for you!)
To answer your questions more specifically:
1) You should throw an exception if the user isn't found that will include the response status 404 (NOT FOUND). Returning null is typically a bad idea as it could mean a lot of things.
1.1?) If your user sends an invalid string, you can look up which exception it causes in your server, and use the exception handler to deal with it and return an appropriate response (BAD_REQUEST maybe?)
2) Yes use long if your use id's are longs.
Check out the baeldung site, would really recommend them for learning Spring Boot.
We are trying to resolve issues related to a security scan. It is considered a vulnerability to expose any information about underlying classes. The scanner is sending invalid data to this endpoint:
#PostMapping(value = "/accountKey", params = "update")
public String accountKeyUpdate(#Valid #ModelAttribute("accountKeyForm") AccountKeyForm key, BindingResult bindingResult, Authentication authentication)
The invalid input looks like this, where "description" is a valid key in the entity, but adding "[]" to the end of the property name in the POST data is causing the parsing error:
description[]:
The server returns the following:
{
"timestamp": "2018-04-20T14:28:36.653Z",
"status": 500,
"error": "Internal Server Error",
"message": "Invalid property 'description[]' of bean class
[com.imsweb.seerapi.account.AccountKeyForm]: Property referenced in indexed property path 'description[]' is neither an array nor a List nor a Map; returned value was []",
"path": "/accountKey/"
}
This is what appears in the log:
org.springframework.beans.InvalidPropertyException: Invalid property 'description[]' of bean class [com.imsweb.seerapi.account.AccountKeyForm]: Property referenced in indexed property path 'description[]' is neither an array nor a List nor a Map; returned value was []
at org.springframework.beans.AbstractNestablePropertyAccessor.processKeyedProperty(AbstractNestablePropertyAccessor.java:375) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:275) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:97) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:839) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.validation.DataBinder.doBind(DataBinder.java:735) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:197) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:107) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:157) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:153) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
The issue is that I cannot find a way to gracefully handle the invalid input. It looks like it happens when the #ModelAttribute is converting the POST body into an AccountKeyForm. That is before it gets inside the controller method. I would prefer to handle the error and just forward them to another page. Alternatively if the message said
"message": "Invalid property 'description[]'"
That would be fine as well.
UPDATE:
I can trap that specific exception using an #ExceptionHandler:
#ControllerAdvice
public class WebControllerAdvice {
#ExceptionHandler(InvalidPropertyException.class)
public String handleBadPropertyException() {
return "error";
}
}
That means I will just get a generic message. That will not pick up other types of exceptions that may fall through the cracks. Is there a better way?
UPDATE:
Here is the entity class. It is a simple bean with two properties.
public class AccountKeyForm {
private String _apiKey;
private String _description;
public AccountKeyForm() {
}
public AccountKeyForm(String apiKey) {
_apiKey = apiKey;
}
public AccountKeyForm(String apiKey, String description) {
_apiKey = apiKey;
_description = description;
}
public String getApiKey() {
return _apiKey;
}
public void setApiKey(String apiKey) {
_apiKey = apiKey;
}
#Size(max = 256)
public String getDescription() {
return _description;
}
public void setDescription(String description) {
_description = description;
}
}
The solution for this is indeed to wrap the exception using the ControllerAdvice, but you need to tweak the response to your needs.
So, instead of returning a String, you should return a full ResponseEntity with a httpStatus and body. The body should be populated with an ErrorResponse where you can define your domain error code if you have something like that and your custom message.
Something like the code below should work.
#ControllerAdvice
public class WebControllerAdvice {
#ExceptionHandler(InvalidPropertyException.class)
public ResponseEntity<ErrorResponse> handle(InvalidPropertyException e) {
return ResponseEntity.status(httpStatus)
.body(new ErrorResponse(errorCode, message));
}
}
public class ErrorResponse {
private final String code;
private final String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
}
It's clearing saying the following
Property referenced in indexed property path 'description[]' is neither an array nor a List nor a Map; returned value was []
Which means that the description field which is being sent from the request is of type array/List/map, so accordingly,you have to change the Model class AccountKeyForm description
from private String _description; to private List<String> _description; or
private Map<String> _description; you will need to figure out what collection type is being sent :)
or you'll have to modify how the request is being sent and ensure that it send only String type and not of List/Map type
The former is an easier solution.
Hope it helps :)
I have a Spring MVC survey application where the Controller method called by each form POST is virtually identical:
#PostMapping("/1")
public String processGroupOne (
Model model,
#ModelAttribute("pageNum") int pageNum,
#ModelAttribute(GlobalControllerAdvice.SESSION_ATTRIBUTE_NAME) #Validated(SurveyGroupOne.class) SurveyCommand surveyCommand,
BindingResult result) {
if (result.hasErrors()) {
LOG.debug(result.getAllErrors().toString());
model.addAttribute("pageNum", pageNum);
return "survey/page".concat(Integer.toString(pageNum));
}
pageNum++;
model.addAttribute("pageNum", pageNum);
return "redirect:/survey/".concat(Integer.toString(pageNum));
}
The only difference is what part of the SurveyCommand object is validated at each stop along the way. This is designated by the marker interface passed to the #Validated() annotation. The marker interfaces (SurveyGroupOne, SurveyGroupTwo, etc) are just that, markers:
public interface SurveyGroupOne {}
public interface SurveyGroupTwo {}
...
and they are applied to properties of objects in the SurveyCommand object:
public class Person {
#NotBlank(groups = {
SurveyGroupTwo.class,
SurveyGroupThree.class})
private String firstName;
#NotBlank(groups = {
SurveyGroupTwo.class,
SurveyGroupThree.class})
private String lastName;
...
}
My question: how can I make the method generic and still use the marker interface specific to the page being processed? Something like this:
#PostMapping("/{pageNum}")
public String processGroupOne (
Model model,
#PathVariable("pageNum") int pageNum,
#ModelAttribute(GlobalControllerAdvice.SESSION_ATTRIBUTE_NAME)
#Validated(__what goes here??__) SurveyCommand surveyCommand,
BindingResult result) {
if (result.hasErrors()) {
LOG.debug(result.getAllErrors().toString());
model.addAttribute("pageNum", pageNum);
return "survey/page".concat(Integer.toString(pageNum));
}
pageNum++;
model.addAttribute("pageNum", pageNum);
return "redirect:/survey/".concat(Integer.toString(pageNum));
}
How can I pass the proper marker interface to #Validated based solely on the pageNum #PathVariable (or any other parameter)?
Because #Validated is an annotation, it requires its arguments to be available during compilation and hence static. You can still use it but in this case you will have N methods, where N is a number of steps. To distinguish one step from another you can use params argument of #PostMapping annotation.
There is also another way where you need to inject Validator to the controller and invoke it directly with an appropriate group that you need.
I am trying to test a method that posts an object to the database using Spring's MockMVC framework. I've constructed the test as follows:
#Test
public void testInsertObject() throws Exception {
String url = BASE_URL + "/object";
ObjectBean anObject = new ObjectBean();
anObject.setObjectId("33");
anObject.setUserId("4268321");
//... more
Gson gson = new Gson();
String json = gson.toJson(anObject);
MvcResult result = this.mockMvc.perform(
post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isOk())
.andReturn();
}
The method I'm testing uses Spring's #RequestBody to receive the ObjectBean, but the test always returns a 400 error.
#ResponseBody
#RequestMapping( consumes="application/json",
produces="application/json",
method=RequestMethod.POST,
value="/object")
public ObjectResponse insertObject(#RequestBody ObjectBean bean){
this.photonetService.insertObject(bean);
ObjectResponse response = new ObjectResponse();
response.setObject(bean);
return response;
}
The json created by gson in the test:
{
"objectId":"33",
"userId":"4268321",
//... many more
}
The ObjectBean class
public class ObjectBean {
private String objectId;
private String userId;
//... many more
public String getObjectId() {
return objectId;
}
public void setObjectId(String objectId) {
this.objectId = objectId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
//... many more
}
So my question is: how to I test this method using Spring MockMVC?
Use this one
public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
#Test
public void testInsertObject() throws Exception {
String url = BASE_URL + "/object";
ObjectBean anObject = new ObjectBean();
anObject.setObjectId("33");
anObject.setUserId("4268321");
//... more
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, false);
ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
String requestJson=ow.writeValueAsString(anObject );
mockMvc.perform(post(url).contentType(APPLICATION_JSON_UTF8)
.content(requestJson))
.andExpect(status().isOk());
}
As described in the comments, this works because the object is converted to json and passed as the request body. Additionally, the contentType is defined as Json (APPLICATION_JSON_UTF8).
More info on the HTTP request body structure
the following works for me,
mockMvc.perform(
MockMvcRequestBuilders.post("/api/test/url")
.contentType(MediaType.APPLICATION_JSON)
.content(asJsonString(createItemForm)))
.andExpect(status().isCreated());
public static String asJsonString(final Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
The issue is that you are serializing your bean with a custom Gson object while the application is attempting to deserialize your JSON with a Jackson ObjectMapper (within MappingJackson2HttpMessageConverter).
If you open up your server logs, you should see something like
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not construct instance of java.util.Date from String value '2013-34-10-10:34:31': not a valid representation (error: Failed to parse Date value '2013-34-10-10:34:31': Can not parse date "2013-34-10-10:34:31": not compatible with any of standard forms ("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "EEE, dd MMM yyyy HH:mm:ss zzz", "yyyy-MM-dd"))
at [Source: java.io.StringReader#baea1ed; line: 1, column: 20] (through reference chain: com.spring.Bean["publicationDate"])
among other stack traces.
One solution is to set your Gson date format to one of the above (in the stacktrace).
The alternative is to register your own MappingJackson2HttpMessageConverter by configuring your own ObjectMapper to have the same date format as your Gson.
I have encountered a similar problem with a more recent version of Spring. I tried to use a new ObjectMapper().writeValueAsString(...) but it would not work in my case.
I actually had a String in a JSON format, but I feel like it is literally transforming the toString() method of every field into JSON. In my case, a date LocalDate field would end up as:
"date":{"year":2021,"month":"JANUARY","monthValue":1,"dayOfMonth":1,"chronology":{"id":"ISO","calendarType":"iso8601"},"dayOfWeek":"FRIDAY","leapYear":false,"dayOfYear":1,"era":"CE"}
which is not the best date format to send in a request ...
In the end, the simplest solution in my case is to use the Spring ObjectMapper. Its behaviour is better since it uses Jackson to build your JSON with complex types.
#Autowired
private ObjectMapper objectMapper;
and I simply used it in my test:
mockMvc.perform(post("/api/")
.content(objectMapper.writeValueAsString(...))
.contentType(MediaType.APPLICATION_JSON)
);
i have to call one controller on the basis of first controller i have to call another controller ......
but this is not working in spring 3 mvc........
#Controller
public class ajaxContoller {
#RequestMapping(value="/mmiFacade",method=RequestMethod.POST)
public #ResponseBody String mmiFacade(#RequestParam String sType){
String forwardName = "";
if (sType.equalsIgnoreCase("Pincode")) {
forwardName = "forward:/pincodeAction";
} else if (sType.equalsIgnoreCase("Locality")) {
forwardName = "forward:/localityAction";
} else if (sType.equalsIgnoreCase("Patient")) {
forwardName = "forward:/patientAction";
} else if (sType.equalsIgnoreCase("Dlhdata")) {
forwardName = "forward:/Dlhdata";
}
return forward;
}
#RequestMapping(value="/pincodeAction",method=RequestMethod.POST)
public #ResponseBody String ajax(){
return "hiii";
}
#RequestMapping(value="/localityAction",method=RequestMethod.POST)
public #ResponseBody String ajax1(){
return "hiii1";
}
}
You should return modelandview object. view name starting with "forward:/" will do the job, otherwise Spring does not even try to interpret the response.
Another option to implement a switch and to invoke other mapping as simple call to anther java function.
Returning a String containing the view name does exactly the same thing as returning a ModelAndView object with the view name set to a String. If you just return a String, Spring internally creates a ModelAndView and set the view name to the value of the String.
In your example, you should not annotate the mmiFacade method with #ResponseBody. Using #ResponseBody bypasses the view resolution process, which is where the "forward:" and "redirect:" prefixes in view names are detected and processed.