Spring #SessionAttributes shared between controllers? - java

I'm using Spring 4.3.7, and I got two form controllers with forms rendering using Spring form taglib in corresponding views. To preserve form data between requests (for rendering invalid forms) I store them in SessionAttributes.
LabCreateController:
#Controller
#RequestMapping(path = "/labs/create")
#SessionAttributes("form")
public class LabCreateController {
#ModelAttribute("form")
public LabCreateForm form() {
return new LabCreateForm();
}
#GetMapping
public String showForm() {
return "lab_create";
}
}
WallController:
#Controller
#RequestMapping(path = "/group/{id}/wall")
#SessionAttributes("form")
public class WallController {
#ModelAttribute("form")
public PostCreateForm form() {
return new PostCreateForm();
}
#GetMapping(path = "/new")
public String newPostGet() {
return "communities_newpost";
}
}
I open /labs/create in browser, everything is fine. Then I open /group/4/wall/new and get a following error:
Invalid property 'text' of bean class
[...LabCreateForm]
i.e it means that attribute form from LabCreateController somehow passed to WallController, though Spring documentation says:
Session attributes as indicated using this annotation correspond to a
specific handler's model attributes.
I believe it means they shouldn't be shared between controllers. Also this answer says that it is so since Spring 3.
Is it a bug or I'm missing something? If not, what is the appropriate way of storing a form inside one controller?

Related

How to map custom/dynamic requests to a controller (Spring)?

How can I map custom/dunamic requests toa given controller, based on a repository lookup?
The use-case is a CMS-like feature in a web-platform, where certain URL patterns ("pages") stored in the DB should be handled by a separate controller PageController.java. These patterns are not necessarily known at compile-time, and they can also be added and modified while the app is deployed (thus, it cannot be annotation-driven).
I did try to map a controller to "**" (see below), but that did not work for 2 reasons: firstly all other requests resolved to that same controller method (I had hoped that it would use "**" as a fallback and try the others first), and it also ended up resolving all requests to my static/asset files to this controller (resulting in unwanted 404-responses).
#Controller
public class PageController {
#Inject
private PageService pageService;
#RequestMapping(value = "**", method = RequestMethod.GET)
public String getPage(Model model, HttpServletRequest request, #CurrentUser User user) {
String path = request.getRequestURI();
Page page = this.pageService.getByPath(path, user);
if (page == null) {
throw new NotFoundException();
}
model.addAttribute("page", page);
return "web/page";
}
}
The temporary work-around/modification to the above method has so far been to map a pre-defined URL-prefixes to this controller (eg. /page/**, /info/**, /news/** etc), but this is an inelegant solution that adds arbitrary limitations to the system which I now seek to eliminate.
I am currently using Spring Boot 2.0. In addition to the naive mapping to ** in a regular #Controller class (using the #RequestMapping -annotation), I have also tried configuring SimpleUrlHandlerMapping the following way:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Inject
private PageDao pageDao;
#Bean
public PageController pageController() {
return new PageController();
}
#Bean
public SimpleUrlHandlerMapping pageUrlHandlerMapping() {
SimpleUrlHandlerMapping pageUrlHandlerMapping = new SimpleUrlHandlerMapping();
PageController pageController = this.pageController();
Map<String, Object> urlMap = this.pageDao.findAll().stream()
.map(Page::getNormalizedSlug)
.collect(Collectors.toMap(Function.identity(),
slug -> pageController, (existing, duplicate) -> existing));
pageUrlHandlerMapping.setUrlMap(urlMap);
pageUrlHandlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE); // <- Cannot be LOWEST_PRECEDENCE for some reason...
return pageUrlHandlerMapping;
}
}
public class PageController implements Controller {
#Inject
private PageService pageService;
#Inject
private DmaWebControllerAdvice controllerAdvice;
#Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
User user = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof User) {
user = (User) principal;
}
String path = request.getRequestURI();
Page page = this.pageService.getByPath(path, user);
if (page == null) {
throw new NotFoundException();
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("web/page");
modelAndView.addObject("page", page);
controllerAdvice.globalModelAttributes(modelAndView.getModel(), null);
return modelAndView;
}
}
This approach does technically work, but the list of pages will somehow have to be reloaded into the SimpleUrlHandlerMapping whenever one of the pages is changed (i am not quite sure how to do that). This also possibly overwrites some default Spring Boot-configuration, that I would ideally like to keep. It also has some drawbacks compared to resolving controllers using #Controller and #RequesMapping because I currently am injecting certain data into all views resolved that way (mainly model-data used in the overall design of the website, like menu, quicklinks etc). In the above attempt, I have had to set those via a separate call to controllerAdvice-globalModelAttributes().
What I am seeking is a solution where my repository is queried for potential page-matches in runtime, and if it is valid then the request will be handled by the proper page-controller. Is a custom HandlerMapping -implementation the way to do this? And if not, how should I solve this? And if making a separate HandlerMapping for pages, how do I add/register this in my configuration without overwriting the default provided by Spring?
Why don't you just implement a catch-all controller which parses your patterns as a parameter, does a db look-up and then use a forward to specific controllers (info, page, news etc.)? Seems like for a CMS, this look-up logic belongs into your code (e.g. service layer).
Easiest(but not the best) way to achieve what you need is creating custom HandlerMapping implementation:
public class PageMapper implements HandlerMapping, Ordered {
private HandlerMethod handlerMethod;
public CustomMapper(Object controller, Method method) {
this.handlerMethod = new HandlerMethod(controller, method);
}
#Override
public HandlerExecutionChain getHandler(HttpServletRequest httpServletRequest) throws Exception {
return new HandlerExecutionChain(handlerMethod);
}
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; //you have to add the handler to the end
}
}
Now remove #Controller annotation from PageController because you don't need it to be detected automatically anymore. After that register controller and mapping to config:
#Configuration
public class AppWebConfig implements WebMvcConfigurer {
#Bean
public PageController pageController() {
return new PageController();
}
#Bean
public HandlerMapping pageMapping(PageController pageController) {
Method method = BeanUtils.resolveSignature("getPage", PageController.class);
return new PageMapping(pageController, method);
}
}
Now every request unrecognized by other HandlerMapping instances will be sent to your mapping hence to your controller. But this approach has obvious disadvantage. Since your mapping is the last in the chain of mappings you never get 404 error. Therefor you never know about something wrong with you resources (e.g. if some of them are missing).
I would prefer let application to distinguish paths by prefix (just like you do it already), where prefix is operation application is going to do with a page. For example if you need to show or edit the page:
#Controller
public class PageController {
private final static String SHOW = "/show";
private final static String EDIT = "/edit";
#Inject
private PageService pageService;
GetMapping(value = SHOW + "/**")
public String getPage(Model model, HttpServletRequest request, #CurrentUser User user) {
String path = request.getRequestURI().substring(SHOW.length());
Page page = this.pageService.getByPath(path, user);
...
model.addAttribute("page", page);
return "web/page";
}
//the same for EDIT operation
}

Is it possible to use two controller classes with one URI in Java Spring?

I m working on this project where am I trying to access two different controllers with the same URI. After trying to run it I m getting a BeanCreationException.
So it happens that I m getting an Error while creating a bean.
I hope there is a way to deal with this.
The error message that I m getting:
org.springframework.beans.factory.BeanCreationException: Error
creating bean with name 'requestMappingHandlerMapping' defined in
class path resource
[org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]:
Invocation of init method failed; nested exception is
java.lang.IllegalStateException: Ambiguous mapping. Cannot map
'userController' method public java.lang.String
com.javalanguagezone.interviewtwitter.controller.UserController.overview(java.security.Principal,org.springframework.ui.Model)
to {[/overview],methods=[GET]}: There is already 'tweetController'
bean method
I m using as well Thymleaf for this project. The URI that I m for those two controllers: http://localhost:8080/api/overview.The two controllers are providing my Thymleaf page with information that i have to present at the same time with the URI just mentioned. With this, I m calling both controllers but I m getting a previously mentioned error.
The first controller class(TweetController):
#Controller
#Slf4j
public class TweetController {
private TweetService tweetService;
public TweetController(TweetService tweetService) {
this.tweetService = tweetService;
}
#GetMapping( "/overview")
public String tweetsFromUser(Principal principal, Model model) {
model.addAttribute("tweets",tweetService.tweetsFromUser(principal).size());
return "api/index";
}
}
The second controller class is:
#Controller
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
#GetMapping("/followers")
public String followers(Principal principal) {
userService.getUsersFollowers(principal);
return "api/index";
}
#GetMapping("/following")
public int following(Principal principal) {
return userService.getUsersFollowing(principal);
}
#GetMapping("/overview")
public String overview(Principal principal, Model model){
model.addAttribute("followers",userService.getUsersFollowers(principal));
model.addAttribute("following",userService.getUsersFollowing(principal));
return "api/index";
} }
My question: is there a way a fix it or I to look for another way around? I m relatively a newbie with Spring. Thank you for your help in advanced.
according to REST conventions, you should not have /overview, but /user/overview. You can set it by supplying #RequestMapping("/user") in your userController.
In the same way you would have "/tweet/overview" endpoint.
#Controller
#Slf4j
#RequestMapping("/tweet")
public class TweetController {
doing it any other way is against conventions, against Spring Rules and probably means you're doing something wrong. Spring does not allow two methods with same uri because it does not know which method exactly you would want to call.
upd: if you need logic, you can send parameters to GET: /overview?customParam=user
#GetMapping( "/overview")
public String tweetsFromUser(#RequestParam(value="customParam") String
param, Principal principal, Model model) {
// logic checking customParam...
But that CANNOT be in two different controllers. The only way to specify the controller, is through base-uri and parameters are not part of it.
Spring determines the method by 2 parameters: Mapping and HTTP method. There is no way, unless you modify Spring manually, to allow 3rd parameter in this case. Also, there is no 3rd parameter.
Alternatively, you can have 3rd controller with Mapping, that calls other 2 controllers when "/overview" endpoint is triggered. In that case, you need to remove the mapping from tweet and user - controllers.

Spring: use different beans in one controller

I have following issue: we've been developing website using Spring tools 3.8.3 and one of the option is to allow users to reset their passwords. In order to do that they need to specify their email address and validate captcha. When these steps are completed data is sent to ResetPassword controller which does all necessary checks and work. Controller has following address:
website.com/path/resetPassword/
And has following structure where it extends abstract controller template to define the input and output beans:
#RequestMapping(path="website.com/path/resetPassword/",
consumes = "application/JSON",
produces = "application/JSON",
method = "POST")
public class ResetPassController extends ControllerTemplate<ResetPassCaptchaBean, ResponseBean>{
// Autowired field declaration
// Couple of methods calling services
}
The bean also has only two fields - email and captcha accordingly. Captcha field uses annotation to check if either it's valid or not:
public class ResetPassCaptchaBean extends ResetPassBean {
#CaptchaValid
private String captcha;
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
public String getCaptcha() {
return captcha;
}
}
public class ResetPassBean {
private String email;
public void setEmail(String email) {
this.email= email;
}
public String getCaptcha() {
return email;
}
}
Now we are adding mobile API and it's necessary to do the same procedure but without using captcha validation. Therefore we're simply using the bean without captcha field. Since captcha is validated using the annotation in the bean it's completely not used in the controller itself and therefore we'd like to have the same controller both for handling requests from web site and from mobile API.
I understand that it's possible to create two different controllers and move all the implementation to service layer (read other class) but then there still would be two identical classes with only one line difference
public class ResetPassController extends ControllerTemplate<ResetPassCaptchaBean, ResponseBean>{
//Autowiring same stuff
//Calling same methods
}
and
public class MobileResetPassController extends ControllerTemplate<ResetPassBean, ResponseBean>{
//Autowiring stuff
//Calling same methods
}
So the question is: leave it with two identical controllers or is there other solution?
Thanks!

How to ensure mandatory parameters are passed into a Spring MVC controller method?

Given a Spring-MVC controller method:
#RequestMapping(value = "/method")
public void method(#RequestParam int param1,
#RequestParam int param2) { /*...*/ }
If parameters are missing in the request URL, an error is reported, e.g:
JBWEB000068: message Required int parameter 'param1' is not present
I need to move the parameters into a single model class yet keep exactly the same GET request URL. So have modified method's parameter to MyModel model, which contains param1 and param2.
This works if #RequestParam is omitted but the snag is no error is reported if parameters are missing. If, on the other hand #RequestParam is included, a parameter named "model" is expected in the GET request. Is there a way to make model parameters mandatory yet keep the same request URL?
Use JSR-303 annotations to validate the object (and don't use primitives but the Object representations in that case).
public class MyObject {
#NotNull
private Integer param1;
#NotNull
private Integer param2;
// Getters / Setters
}
Controller method.
#RequestMapping(value = "/method")
public void method(#Valid MyObject obj) { /*...*/ }
If you don't have a JSR-303 provider (hibernate-validator for instance) on your classpath create a Validator and use this to validate your object.
Links.
Spring MVC reference guide
Spring Validation reference guide
You can try #ModelAttribute and method=RequestMethod.POST:
In your controller:
//...
#RequestMapping(value="/method", method=RequestMethod.POST)
public String add(#ModelAttribute("modelName") ModelClass form)
{
//.. your code
}
//..
In your JSP:
<%# taglib uri="http://www.springframework.org/tags/form" prefix="f" %>
//.. some code
<f:form action="method" method="post" modelAttribute="modelName">
</f:form>
If you are restricted to GET requests only, you might not be able to use a separate ModelClass. You need to resort to request parameters in that case.

Spring: Controller #RequestMapping to jsp

Using Spring 3.1.2
How do you reference the Annotated value of the CONTROLLER (not method) RequestMapping from a jsp so I can build URL's relative to the Controller. If the method level request mappings are referenced, it will cause my links to not work, so they cannot be part of the this in any way.
For example (Note that this controller's mapping is "/foo/test"):
#Controller
#RequestMapping("/foo/test")
public class FooController {
#RequestMapping(value="/this", method=RequestMethod.GET)
public String getThis() {
return "test/this";
}
#RequestMapping(value="/that", method=RequestMethod.GET)
public String getThat() {
return "test/that";
}
#RequestMapping(value="/other", method=RequestMethod.GET)
public String getOther() {
return "test/other";
}
}
...and from my this.jsp:
<%# taglib uri="http://www.springframework.org/tags" prefix="s"%>
<s:url value="${controllerRequestMapping}" var="baseUrl"/>
That
Other
The reason I need to access the RequestMapping is because my views may be accessed from multiple Controllers. Note that this controller's mapping is "/bar/test"!
#Controller
#RequestMapping("/bar/test")
public class BarController {
#RequestMapping(value="/this", method=RequestMethod.GET)
public String getThis() {
return "test/this";
}
#RequestMapping(value="/that", method=RequestMethod.GET)
public String getThat() {
return "test/that";
}
#RequestMapping(value="/other", method=RequestMethod.GET)
public String getOther() {
return "test/other";
}
}
Some of my Controller level request mappings have path variables too, so getting just the string value of the request mapping will not work. It will have to be resolved:
#Controller
#RequestMapping("/something/{anything}/test/")
public class SomethingController {
...
}
Update
Maybe if there was a way to modify the context path by appending the Controller request mapping to it BEFORE the Spring URL tag, that would solve the problem.
contextPath = contextPath/controllerRequestMapping
Then, I could do something like this because I believe Spring will automatically retrieve the current context path:
<%# taglib uri="http://www.springframework.org/tags" prefix="s"%>
That
Other
This, of course, would be optimal! Ideas? Thoughts...?
Thanks in advance!
You could get the URI using the Servlet API. There is no "Spring" way to do this as far as I know.
#RequestMapping(value="/this", method=RequestMethod.GET)
public String getThis(HttpServletRequest request, Model m) {
m.addAttribute("path", request.getRequestURI());
return "test/this";
}
Then in your JSP:
This
For more information about HttpServletRequest, see the API here.
There is no built-in way to access the #RequestMapping, but you can simply add the URL to the model and reference it from the view that way.
According to me there is no difference in putting #RequestMapping annotation at both class level and method level. Instead of that you can have the combined #RequestMapping annotation on top of the method only.
So now your method level annotation will be something like this.
#RequestMapping(value="("/foo/test/this", method=RequestMethod.GET)
or
#RequestMapping(value="("/bar/test/this", method=RequestMethod.GET)
Now since both your methods return the same view you can have just one method for both of them. And in the annotation mapping value instead of foo and bar you can inroduce one path variable {from}. So finally your annotation and the method will be like this.
#RequestMapping(value="("/{from}/test/this", method=RequestMethod.GET)
public String getThis(#PathVariable("from") String from) {
return "test/this";
}
And after doing this in your method you can perform different calculations or operations on based of the runtime value of path variable from you get. And in the JSP page you can simple get this value with ${from}.
Hope this helps you. Cheers.
See code below to access request mapping, if you are using Spring 3.1 and above. I'm not sure if this is what you require. This is just an attempt and of course i do not know of any straight forward way. Invoke getRequestMappings() from inside a method that has request mapping.
public class FooController{
private RequestMappingHandlerMapping handlerMapping = null;
#Autowired
public FooController(RequestMappingHandlerMapping handlerMapping){
this.handlerMapping = handlerMapping;
}
public Set<String> getRequestMappings(){
Map<RequestMappingInfo, HandlerMethod> handlerMap = handlerMapping.getHandlerMethods();
Iterator<RequestMappingInfo> itr = handlerMap.keySet().iterator();
while(itr.hasNext()){
RequestMappingInfo info = itr.next();
PatternsRequestCondition condition = info.getPatternsCondition();
Set<String> paths = condition.getPatterns();
HandlerMethod method = handlerMap.get(info);
if(method.getMethod().getName().equals(Thread.currentThread().getStackTrace()[2].getMethodName()))
return paths;
}
return new HashSet<String>();
}
}
As of 4.1 every #RequestMapping is assigned a default name based on the capital
letters of the class and the full method name. For example, the method getFoo
in class FooController is assigned the name "FC#getFoo". This naming strategy isenter code here
pluggable by implementing HandlerMethodMappingNamingStrategy and configuring it on your
RequestMappingHandlerMapping. Furthermore the #RequestMapping annotation includes a
name attribute that can be used to override the default strategy.
The Spring JSP tag library provides a function called mvcUrl that can be used to prepare links to
controller methods based on this mechanism.
For example given:
#RequestMapping("/people/{id}/addresses")
public class MyController {
#RequestMapping("/{country}")
public HttpEntity getAddress(#PathVariable String country) { ... }
}
The following JSP code can prepare a link:
<%# taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
Get Address
Make sure you have configured your MVC using either Java Config or XML way and not both the ways.
You will get an exception as :more than one RequestMappingInfo HandlerMapping found.

Categories