When modifying a ModelAttribute that is listed as a SessionAttribute, why doesent it keep its new value?
Every time I make a request to the example below, it prints out "Initial value.", which is a correct value for the first request. But after the first request, its value should be "new value".
Why doesent ModelAttribute store its value?
I have a base class. All servlets extending this:
#SessionAttributes(value = {"test_string", "something"})
public abstract class Base<T>
{
public abstract T request(
#ModelAttribute("test_string") String _test_string,
ModelAndView _mv);
#ModelAttribute("test_string")
private String getTest()
{
return "Initial value.";
}
}
I have a specific servlet:
#Controller
public class InterfaceController extends Base<String>
{
#PostMapping(value = "/interface")
#ResponseBody
#Override
public String request(
#ModelAttribute("test_string") String _test_string,
ModelAndView _mv)
{
System.out.println(_test_string);
_test_string = "new value";
return "whatever content";
}
}
I'm no Spring MVC expert but your problem seems to be understanding Java pass-by-reference and String inmutability. I've made a diagram to help you understand what the problem is but you may need to research more info about it.
When you invoke sysout, you are printing the value pointed by "_test_string" (method argument), that at this point is the same that ModelAttribute "test_string".
When you assign "new value" to "_test_string" (method argument), notice that you're NOT changing the value of "test_string" (ModelAttribute)
It's what I think you have to do to overwrite the value stored in the Model.
Related
Okay so I am using a payment service called Thrive cart, I am doing this for a membership website I'm creating. When the user has paid I want them to be redirected to a URL where I can use that data to update the current users information.
The data that get's sent in the params is insane:
http://localhost:5000/user/welcome?thrivecart%5Baccount_id%5D=3196&thrivecart%5Baccount_name%5D=testacount&thrivecart%5Bcustomer%5D%5Bemail%5D=testname8%40gmail.com&thrivecart%5Bcustomer%5D%5Baddress%5D%5Bcountry%5D=GB&thrivecart%5Bcustomer%5D%5Baddress%5D%5Bzip%5D=pe303wu&thrivecart%5Border%5D%5B0%5D%5Bt%5D=product&thrivecart%5Border%5D%5B0%5D%5Bid%5D=6&thrivecart%5Border%5D%5B0%5D%5Bn%5D=Monthly+membership&thrivecart%5Border%5D%5B0%5D%5Bp%5D=799&thrivecart%5Border%5D%5B0%5D%5Bq%5D=1&thrivecart%5Border%5D%5B0%5D%5Bpo%5D=60120&thrivecart%5Border%5D%5B1%5D%5Bt%5D=product&thrivecart%5Border%5D%5B1%5D%5Bid%5D=6&thrivecart%5Border%5D%5B1%5D%5Bn%5D=Monthly+membership&thrivecart%5Border%5D%5B1%5D%5Bp%5D=799&thrivecart%5Border%5D%5B1%5D%5Bq%5D=1&thrivecart%5Border%5D%5B1%5D%5Bpo%5D=60120&thrivecart%5Border_currency%5D=GBP&thrivecart%5Border_id%5D=1041278&thrivecart%5Border_tax%5D=0.2&thrivecart%5Border_tax_id%5D=gb&thrivecart%5Border_total%5D=799&thrivecart%5Bpayment_processor%5D=paypal&thrivecart%5Bproduct_id%5D=6&thrivecart%5Bpurchases%5D%5B0%5D=6&thrivecart%5Bpurchases%5D%5B1%5D=6&thrivecart_hash=a5b711d2288b4cb587511811bc0a3473
So far I've set up a simple controller which doesn't get hit:
#RestController
#RequestMapping("/user")
public class UserController {
#RequestMapping(value = "/welcome", method = RequestMethod.POST)
public void welcomeMember(#PathVariable String data) {
System.out.println(data);
}
}
How do I deal with crazy data like this? Do I have to specific each path param?
First of all, what you seem to get are not path elements but request parameters, so you will need #RequestParam annotations to get the values.
Since there are so many request parameters, I would also recommend to take just one parameter, a Map<String, String>. That Map will contain all the parameters as key/value pairs, for example:
key: "thrivecart[account_id]"
value: "3196"
If you're not sure whether you receive a POST or a GET request, you can also add a second parameter to receive the HttpMethod.
Change your RestController to:
#RestController
#RequestMapping("/user")
public class UserController {
#RequestMapping(value = "/welcome")
public void welcomeMember(#RequestParam Map<String, String> data, HttpMethod method) {
System.out.println(method);
System.out.println(data);
}
}
That looks like a problem with how the rest api is called from the service consumer side.
try sending the data in a request body rather then as a param. This way you can use a POJO to handle the data.
Question 1: So far I've set up a simple controller which doesn't get hit:
As per your URL http://localhost:5000/user/welcome "user" seems to be your projects context name. Try removing #RequestMapping("/user") from your class.
Also, instead of#PathVariable String data use #RequestParam Map<String,String> params. #PathVariable String data is used when data is part of url but in your case it's parameter. Final code should be like below.
#RestController
public class UserController {
#RequestMapping(value = "/welcome", method = RequestMethod.POST)
public void welcomeMember(#RequestParam Map<String,String> params ) {
for(Map.Entry<String, String> entry : params.entrySet()){
//This will print all paramters name and their value
System.out.println(entry.getKey() + "-" + entry.getValue());
}
}
}
Question 2: How do I deal with crazy data like this? Do I have to specific each path param?
I will suggest to follow standard practice. Send data in json format. There are different ways for this depend upon front end technology you are using. One way is Link
I want to pass multiple variable parameters (including none) for a #ModelAttribute evaluation from html thymeleaf page to a method:
//provide 0 or X params
#ModelAttribute("url")
public String url(String... params) {
return "generatedurl";
}
The following thymeleaf statements should be possible:
th:href="#{${url()}"
th:href="#{${url('page')}}"
th:href="#{${url('page', 'sort')}}"
But it does not work. Why?
#ModelAttribute is used to bind common objects to the model.
You are returning a String generatedurl every time from your method url() annotated with #ModelAttribute. So in your Thymleaf view every time yo do ${url} you get generatedurl.
One workaround for your problem could be this
#ModelAttribute("url")
public void url(Model model) {
model.addAttribute("url","YOUR_URL");
model.addAttribute("sort","age");
}
As a workaround: add the enclosing class as a model parameter, and call the method on this param from thymeleaf:
#Controller
public class PageController {
#GetMapping
public String persons(Model model) {
model.addAttribute("util", this);
return "persons";
}
public String url(String... params) {
//evaluate...
return "generatedurl";
}
}
It's then possible to access the methods as normal:
th:href="#{${url()}"
th:href="#{${url('page')}}"
th:href="#{${url('page', 'sort')}}"
Only drawback is of course having to add the class to the model explicit.
Another choice would be to initialize the model with the parameter permutations that I need. But that's less flexible:
#ModelAttribute
public void init(Model model) {
model.addAttribute("plainUrl", ...);
model.addAttribute("pageUrl", ...);
model.addAttribute("pageSizeUrl", ...);
}
I have the following controller. I am using Spring to create Restful APIs.
#RestController
public class UserController extends RestControlValidator {
#RequestMapping(value = "/user/", method = RequestMethod.POST, headers = "Accept=application/json", consumes = "application/json", produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody List newUser(#RequestBody #Valid UserInput input,BindingResult result)
{Some code}
}
The UserInput class looks like this:
public class UserInput{
#NotEmpty
private String emailId;
#NotEmpty
private String fName;
private String lName;
private int sex;
//getters and setters
Now when I try and access /user/ with data {"sex":"Male"}, I get the following response:
I want the response in case of such a request to be:
{"errors":{"sex":"The value must be an integer"}}
Is there any way of customising BAD REQUEST responses in Spring?
Considering the current scenario the most ideal solution would be to alter the behavior of HandlerMethodArgumentResolve as the json to pojo constructed by #RequestBody fails because we dont get a chance to check the wrong data and this check can very well be done in the custom message converter
A. first we would need to create LanguageMessageConverter as follows
public class LanguageMessageConverter extends
AbstractHttpMessageConverter<Language> {
private Gson gson = new Gson();
public LanguageMessageConverter() {
super(new MediaType("application", "json", Charset.forName("UTF-8")));
}
#Override
protected boolean supports(Class<?> clazz) {
return Language.class.equals(clazz);
}
Map<String, String> mp = new HashMap<>();
#Override
protected Language readInternal(Class<? extends Language> clazz,
HttpInputMessage httpInputMessage) throws IOException,
HttpMessageNotReadableException {
Map langmp = gson.fromJson(
convertStreamToString(httpInputMessage.getBody()), Map.class);
for (Field field : clazz.getDeclaredFields()) {
if (!langmp.get(field.getName()).getClass().getCanonicalName().equals(field.getType().getCanonicalName())) {
if (field.getType().getCanonicalName().equals("java.lang.Integer")||field.getType().getCanonicalName().toString().equals("int")) {
langmp.put(field.getName(), "0");
} else if (field.getType().equals("java.lang.String")) {
//TODO COde needs to be improved here because this check is not efficient
langmp.put(field.getName(), "wrong");
}
}
}
Language lang = gson.fromJson(gson.toJson(langmp), clazz);
return lang;
}
we need to set the media type new MediaType("application", "json", Charset.forName("UTF-8")) which will make sure this class intervenes the mentioned MIME type
Considering we need to manipulate the result I found it best to convert it to map langmp (There are better JSON Parsers which can be used)
Since we need to to understand the existing type I used reflection api to get the fields via getDeclaredFields()
Using the above made the logical check using the datatype to understand if the type is incorrect for eg if the field datatype is int and if it is found as String then corresponding map value will be substituted
once that is done the map will hold the updated values where in if the data was wrong a default value would be set eg if the int var is set to 0 since the originating json had a String in it.
Once that is done the updated map is converted to the concerned class.
B. Secondly we need to register the custom MessageConverter in the dispatcher xml i.e. LanguageMessageConverter
<mvc:annotation-driven >
<mvc:message-converters register-defaults="true">
<bean class="com.comp.org.controller.LanguageMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
register-defaults="true" is very important since we are adding Custom MessageConverter but we also need the other existing converters working along with the one we have added
LanguageMessageConverter needs to be registered here.
C. Considering the concerned pojo is populated with the necessary details it would reach our controller post processing in the custom converter now we would add the manual validation eg. if the int variable has 0 the necessary error json should be returned
As per your request even if the json consists of the wrong data the custom message converter should process it and accordingly in the controller we can validate the condition mentioned.
The code definitely can be improved further. Kindly let me know if this solution fulfilled your requirement or any part of the code requires further elaboration and hopefully addressed your concern.
I had the same issue, than I solved that way:
Create an Object called Error, like that (don't forget to implement Serializable...):
private String fieldName;
private String errorCode;
private String defaultMessage;
public Error() {
}
public Error(String fieldName, String errorCode, String defaultMessage) {
this.fieldName = fieldName;
this.errorCode = errorCode;
this.defaultMessage = defaultMessage;
}
/* getters, setters */
Inside the #RestController method you ave to call inputValidator.validate() method (if you didn't create an Object Validator for your UserInput then we're really don't speaking the same language...)
// validating the userInput
userInputValidator.validate(userInput, bindingResult);
if (bindingResult.hasErrors()) {
List<Error> errors = new ArrayList<>(bindingResult.getErrorCount());
for (FieldError fieldWithError : bindingResult.getFieldErrors()) {
errors.add(new Error(fieldWithError.getField(), fieldWithError.getCode(), fieldWithError.getDefaultMessage()));
}
return errors;
}
// in case of success:
return null;
Finally you'll have to translate the JSON object to your client side. You'll have two kind of objects:
3.1. null (undefined depending on the language you're using)
3.2. A JSON object like that:
[
{
"fieldName": "name",
"errorCode": "user.input.name.in.blank",
"defaultMessage": "Insert a valid name!"
},
{
"fieldName": "firstPhone",
"errorCode": "user.input.first.phone.blank",
"defaultMessage": "Insert a valid first phone!"
}
]
My question is essentially a follow-up to this question.
#RestController
public class TestController
{
#RequestMapping("/getString")
public String getString()
{
return "Hello World";
}
}
In the above, Spring would add "Hello World" into the response body. How can I return a String as a JSON response? I understand that I could add quotes, but that feels more like a hack.
Please provide any examples to help explain this concept.
Note: I don't want this written straight to the HTTP Response body, I want to return the String in JSON format (I'm using my Controller
with RestyGWT which requires the response to be in valid JSON
format).
Either return text/plain (as in Return only string message from Spring MVC 3 Controller) OR wrap your String is some object
public class StringResponse {
private String response;
public StringResponse(String s) {
this.response = s;
}
// get/set omitted...
}
Set your response type to MediaType.APPLICATION_JSON_VALUE (= "application/json")
#RequestMapping(value = "/getString", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
and you'll have a JSON that looks like
{ "response" : "your string value" }
JSON is essentially a String in PHP or JAVA context. That means string which is valid JSON can be returned in response. Following should work.
#RequestMapping(value="/user/addUser", method=RequestMethod.POST)
#ResponseBody
public String addUser(#ModelAttribute("user") User user) {
if (user != null) {
logger.info("Inside addIssuer, adding: " + user.toString());
} else {
logger.info("Inside addIssuer...");
}
users.put(user.getUsername(), user);
return "{\"success\":1}";
}
This is okay for simple string response. But for complex JSON response you should use wrapper class as described by Shaun.
In one project we addressed this using JSONObject (maven dependency info). We chose this because we preferred returning a simple String rather than a wrapper object. An internal helper class could easily be used instead if you don't want to add a new dependency.
Example Usage:
#RestController
public class TestController
{
#RequestMapping("/getString")
public String getString()
{
return JSONObject.quote("Hello World");
}
}
You can easily return JSON with String in property response as following
#RestController
public class TestController {
#RequestMapping(value = "/getString", produces = MediaType.APPLICATION_JSON_VALUE)
public Map getString() {
return Collections.singletonMap("response", "Hello World");
}
}
Simply unregister the default StringHttpMessageConverter instance:
#Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
/**
* Unregister the default {#link StringHttpMessageConverter} as we want Strings
* to be handled by the JSON converter.
*
* #param converters List of already configured converters
* #see WebMvcConfigurationSupport#addDefaultHttpMessageConverters(List)
*/
#Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(c -> c instanceof StringHttpMessageConverter);
}
}
Tested with both controller action handler methods and controller exception handlers:
#RequestMapping("/foo")
public String produceFoo() {
return "foo";
}
#ExceptionHandler(FooApiException.class)
public String fooException(HttpServletRequest request, Throwable e) {
return e.getMessage();
}
Final notes:
extendMessageConverters is available since Spring 4.1.3, if are running on a previous version you can implement the same technique using configureMessageConverters, it just takes a little bit more work.
This was one approach of many other possible approaches, if your application only ever returns JSON and no other content types, you are better off skipping the default converters and adding a single jackson converter. Another approach is to add the default converters but in different order so that the jackson converter is prior to the string one. This should allow controller action methods to dictate how they want String to be converted depending on the media type of the response.
I know that this question is old but i would like to contribute too:
The main difference between others responses is the hashmap return.
#GetMapping("...")
#ResponseBody
public Map<String, Object> endPointExample(...) {
Map<String, Object> rtn = new LinkedHashMap<>();
rtn.put("pic", image);
rtn.put("potato", "King Potato");
return rtn;
}
This will return:
{"pic":"a17fefab83517fb...beb8ac5a2ae8f0449","potato":"King Potato"}
Make simple:
#GetMapping("/health")
public ResponseEntity<String> healthCheck() {
LOG.info("REST request health check");
return new ResponseEntity<>("{\"status\" : \"UP\"}", HttpStatus.OK);
}
Add produces = "application/json" in #RequestMapping annotation like:
#RequestMapping(value = "api/login", method = RequestMethod.GET, produces = "application/json")
Hint: As a return value, i recommend to use ResponseEntity<List<T>> type. Because the produced data in JSON body need to be an array or an object according to its specifications, rather than a single simple string. It may causes problems sometimes (e.g. Observables in Angular2).
Difference:
returned String as json: "example"
returned List<String> as json: ["example"]
Add #ResponseBody annotation, which will write return data in output stream.
This issue has driven me mad: Spring is such a potent tool and yet, such a simple thing as writing an output String as JSON seems impossible without ugly hacks.
My solution (in Kotlin) that I find the least intrusive and most transparent is to use a controller advice and check whether the request went to a particular set of endpoints (REST API typically since we most often want to return ALL answers from here as JSON and not make specializations in the frontend based on whether the returned data is a plain string ("Don't do JSON deserialization!") or something else ("Do JSON deserialization!")). The positive aspect of this is that the controller remains the same and without hacks.
The supports method makes sure that all requests that were handled by the StringHttpMessageConverter(e.g. the converter that handles the output of all controllers that return plain strings) are processed and in the beforeBodyWrite method, we control in which cases we want to interrupt and convert the output to JSON (and modify headers accordingly).
#ControllerAdvice
class StringToJsonAdvice(val ob: ObjectMapper) : ResponseBodyAdvice<Any?> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean =
converterType === StringHttpMessageConverter::class.java
override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
return if (request.uri.path.contains("api")) {
response.getHeaders().contentType = MediaType.APPLICATION_JSON
ob.writeValueAsString(body)
} else body
}
}
I hope in the future that we will get a simple annotation in which we can override which HttpMessageConverter should be used for the output.
Simple and Straightforward send any object or return simple List
#GetMapping("/response2")
#ResponseStatus(HttpStatus.CONFLICT)
#ResponseBody List<String> Response2() {
List<String> response = new ArrayList<>(Arrays.asList("Response2"));
return response;
}
I have added HttpStatus.CONFLICT as Random response to show how to pass RequestBody also the HttpStatus
Annotate your method with the #ResponseBody annotation to tell spring you are not trying to render a view and simple return the string plain
I'm testing out the #InitBinder annotation so I can have String objects converted into appropriate Enum objects during web requests.
I created the following simple Enum:
SampleEnum.java
public enum SampleEnum {
ONE,
TWO,
THREE,
FOUR,
FIVE;
}
Then, I created an editor extending PropertyEditorSupport to be called from the #InitBinder code:
EnumPropertyEditor.java
#SuppressWarnings("rawtypes")
public class EnumPropertyEditor extends PropertyEditorSupport {
private Class clazz;
public EnumPropertyEditor(Class clazz) {
this.clazz = clazz;
}
#Override
public String getAsText() {
return (getValue() == null ? "" : ((Enum) getValue()).name());
}
#SuppressWarnings("unchecked")
#Override
public void setAsText(String text) {
Enum e = Enum.valueOf(clazz, text);
setValue(e);
}
}
Then, in my controller I added the #InitBinder and a simple request mapping:
Controller
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(SampleEnum.class, new EnumPropertyEditor(SampleEnum.class));
}
#RequestMapping(method = POST, value = "/postSampleEnum")
#ResponseBody
public SampleEnum postSampleEnum(#RequestBody SampleEnum sampleEnum) {
return sampleEnum;
}
From my understanding, a request for this controller method should attempt to convert a string value into the SampleEnum object. However, no breakpoints are hit in either initBinder, request mapping method, nor any of the methods in the EnumPropertyEditor.
I'm testing with RESTClient in FireFox, and have tried sending in the request body "THREE", which I would expect to work. Instead, I get a 415 error regardless of the what's in the request body. (The server refused this request because the request entity is in a format not supported by the requested resource for the requested method ().)
If I change the request mapping to take in a string instead of a SampleEnum, the postSampleEnum gets called and doesn't use the custom editor (as expected).
Am I missing anything that allows the custom editor code to be called? What is the best way to continue debugging this?
First of all, I forgot to add the application/json content-type to the request header in RESTClient. >_<
However, I noticed that the code execution still doesn't go through the custom property editor. As GriffeyDog said, it looks like the code only executes if I switch to a RequestParam or ModelAttribute.