In Spring Framework , AbstractWizardFormController seems deprecated. How to implement multiple pages form in the Spring MVC Framework. (I am not using webflow)
any example or pointer would help considering my limited knowledge in Spring.
A #Controller is a more flexible way to define a form / wizard. You are supposed to map methods to requests based on requested path / request parameters / request method. So instead of defining a list of views and processing the request based on some required "step" parameter, you can define the steps of your wizard as you wish (also the command object will be handled more transparently). Here's how you can get to emulate a classic AWFC functionality (this is only meant to be an example, there's a lot more you can do).
#Controller
#RequestMapping("/wizard.form")
#SessionAttributes("command")
public class WizardController {
/**
* The default handler (page=0)
*/
#RequestMapping
public String getInitialPage(final ModelMap modelMap) {
// put your initial command
modelMap.addAttribute("command", new YourCommandClass());
// populate the model Map as needed
return "initialView";
}
/**
* First step handler (if you want to map each step individually to a method). You should probably either use this
* approach or the one below (mapping all pages to the same method and getting the page number as parameter).
*/
#RequestMapping(params = "_step=1")
public String processFirstStep(final #ModelAttribute("command") YourCommandClass command,
final Errors errors) {
// do something with command, errors, request, response,
// model map or whatever you include among the method
// parameters. See the documentation for #RequestMapping
// to get the full picture.
return "firstStepView";
}
/**
* Maybe you want to be provided with the _page parameter (in order to map the same method for all), as you have in
* AbstractWizardFormController.
*/
#RequestMapping(method = RequestMethod.POST)
public String processPage(#RequestParam("_page") final int currentPage,
final #ModelAttribute("command") YourCommandClass command,
final HttpServletResponse response) {
// do something based on page number
return pageViews[currentPage];
}
/**
* The successful finish step ('_finish' request param must be present)
*/
#RequestMapping(params = "_finish")
public String processFinish(final #ModelAttribute("command") YourCommandClass command,
final Errors errors,
final ModelMap modelMap,
final SessionStatus status) {
// some stuff
status.setComplete();
return "successView";
}
#RequestMapping(params = "_cancel")
public String processCancel(final HttpServletRequest request,
final HttpServletResponse response,
final SessionStatus status) {
status.setComplete();
return "canceledView";
}
}
I tried to vary the method signatures so that you can get an idea about the flexibility I mentioned. Of course, there's a lot more to it: you can make use of request method (GET or POST) in the #RequestMapping, you can define a method annotated with #InitBinder, etc.
EDIT: I had an unmapped method which I fixed (by the way, you need to make sure you don't have ambiguous mappings -- requests that could be mapped to more than one method -- or unmapped requests -- requests that cannot be mapped to any method). Also have a look at #SessionAttributes, #SessionStatus and #ModelAttribute, which are also needed for fully simulating the behaviour of the classic AWFC (I edited the code already to make this clear).
Related
I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.
I have a huge form with around 30 parameters and I don't think it's a good idea to do what I usually do.
The form will be serialized and pass all the parameters via ajax post to spring controller.
I usually do like this:
#RequestMapping(value = "/save-state", method = RequestMethod.POST)
public #ResponseBody
void deleteEnvironment(#RequestParam("environmentName") String environmentName, #RequestParam("imageTag") String imageTag) {
//code
}
but if I have 30 parameters I will have a huge parameter list in the function.
What is the usual and correct way to avoid this?
EDIT: What if I pass the HttpServlet request only?? The request will have all the parameters and I can simple call request.getParameters("").
There are two options I would suggest:
Take an HttpServletRequest object and fetch needed properties separately.
The problem is Spring's controllers are designed to eliminate such low-level API (Servlets API) calls. It's could be the right fit if a controller was too abstract (operates on abstract datasets), which means you wouldn't be able to define a DTO with a fixed-length number of parameters.
Construct a DTO class with the properties needed and take it as a parameter.
The advantage is you completely delegate low-level work to Spring and care only about your application logic.
You can do something like this:
#RequestMapping(value = "/save-state", method = RequestMethod.POST)
public void deleteEnvironment(#RequestBody MyData data) {
//code
}
Create a class containing all your form parameters and receive that on your method.
but if I have 30 parameters I will have a huge parameter list in the
function.
In your request, pass a JSON object that contains these information and create its counterpart in Java.
RequestMethod.POST is not design to perform deletion.
Usr rather RequestMethod.DELETE.
#RequestMapping(value = "/save-state", method = RequestMethod.DELETE)
public #ResponseBody
void deleteEnvironment(MyObject myObject) {
//code
}
Correct way is to serialize all parameters as Json and call back end api with one parameter.
On back-end side get that json and parse as objects.
Example:
` #RequestMapping(method = POST, path = "/task")
public Task postTasks(#RequestBody String json,
#RequestParam(value = "sessionId", defaultValue = "-1") Long sessionId)
throws IOException, AuthorizationException {
Task task = objectMapper.readValue(json, Task.class);
`
My project with roo generated CRUD is working ok, but now i need to change the way some entities are saved (For example i have a "User" attribute that i want set dynamically as the user logged)
At the moment i'm just moving the save() method from the aspect to the .java and modifying it as needed. It worked well so far but the roo console don't seem to like it, as it recreate the method in the aspect as soon as i change the method return type or other things.
I don't need an specific answer to this example, instead i'm looking to know if this is the best approach to modify/override the out-of-the-box create/show functionality for the entities provided by roo.
Edit: Added Example
One of my entities is "Servicio", it have some "ServiciosCollectionThymeleafController_Roo_Thymeleaf.aj" with a "ServiciosCollectionThymeleafController.create" method. I proceeded to push in all the .aj into the "ServiciosCollectionThymeleafController.java"
Then i made some minor changes in the create method and saved it. It worked, but when i opened the roo console the consoled generated the pushed aj again, just with the method i edited earlier.
Original create method on the aspect:
/**
* TODO Auto-generated method documentation
*
* #param servicio
* #param result
* #param model
* #return ModelAndView
*/
#PostMapping(name = "create")
public ModelAndView ServiciosCollectionThymeleafController.create(#Valid #ModelAttribute Servicio servicio, BindingResult result, Model model) {
if (result.hasErrors()) {
populateForm(model);
return new ModelAndView("/servicios/create");
}
Servicio newServicio = getServicioService().save(servicio);
UriComponents showURI = getItemLink().to(ServiciosItemThymeleafLinkFactory.SHOW).with("servicio", newServicio.getId()).toUri();
return new ModelAndView("redirect:" + showURI.toUriString());
}
The same method pushed in into the .java, and my modifications:
/**
* TODO Auto-generated method documentation
*
* #param servicio
* #param result
* #param model
* #return ModelAndView
*/
#PostMapping(name = "create")
public ModelAndView create(#Valid #ModelAttribute Servicio servicio, BindingResult result, Model model, Principal principal, Pageable pageable) {
if (result.hasErrors()) {
populateForm(model);
return new ModelAndView("/servicios/create");
}
Prestador current = (Prestador) personaService.findByUsername(principal.getName(), pageable).getContent().get(0);
if (current == null) {
populateForm(model);
return new ModelAndView("/servicios/create");
}
servicio.setPrestador(current);
Servicio newServicio = getServicioService().save(servicio);
return new ModelAndView("redirect:/ver-servicio/" + newServicio.getId());
}
Thanks.
Roo checks if a method is already included in the Java file by looking for the method signature (the method's name and the parameter types), as this is the way supported in java for overloading methods.
In your case, once you change the create method parameters, it is no longer the same method signature, and that's why Roo generates it again.
In general this is not a problem, as you have to change the clients of that method to use the new one. For example, if you add a new method to a Service, you will change also the implementation of the Controller to use that new method, and the one generated by Roo will not affect you.
In the case of controller methods the problem is related to the mapping. In your case you end up with two methods, the one added by you and the one generated by Roo, with the same request mapping. To solve it you just have to add the method generated by Roo without the mapping annotation.
In your case the code would be the following one:
public ModelAndView create(#Valid #ModelAttribute Servicio servicio, BindingResult result, Model model) {
throw new UnsupportedOperationException();
}
#PostMapping(name = "create")
public ModelAndView create(#Valid #ModelAttribute Servicio servicio, BindingResult result, Model model, Principal principal, Pageable pageable) {
if (result.hasErrors()) {
populateForm(model);
return new ModelAndView("/servicios/create");
}
Prestador current = (Prestador) personaService.findByUsername(principal.getName(), pageable).getContent().get(0);
if (current == null) {
populateForm(model);
return new ModelAndView("/servicios/create");
}
servicio.setPrestador(current);
Servicio newServicio = getServicioService().save(servicio);
return new ModelAndView("redirect:/ver-servicio/" + newServicio.getId());
}
As you have the create method with the default signature, it won't be regenerated by Roo. Also, it doesn't have the PostMapping annotation, so it will be ignored by Spring MVC which will call the create method with the new signature.
Additional note:
Also you will have to change how the link to the method is generated. All Thymeleaf controllers generated by Roo have a companion class (ending with LinkFactory) which is used to generate the links to that controller methods, avoiding the use of hardcoded URIs in the Thymeleaf pages as well as Controller redirects. Those LinkFactory classes are generated using the Spring's MvcUriComponentsBuilder.fromMethodCall utility which uses a fake method call to generate the link to that Controller method.
As you have a new method signature, you have to change the default implementation of the ServiciosCollectionThymeleafController toUri method. Push-in the toUri method to the Java file and change the implementation to something like this.
public UriComponents toUri(String methodName, Object[] parameters, Map<String, Object> pathVariables) {
...
if (methodName.equals(CREATE)) {
return SpringletsMvcUriComponentsBuilder.fromMethodCall(SpringletsMvcUriComponentsBuilder.on(getControllerClass()).create(null, null, null, null, null)).buildAndExpand(pathVariables);
}
...
}
Note I've added two additional null parameters to the create method call use the new method signature. With this change, all URIs generated from the Thymeleaf pages will point to the new method.
Is there a good way within the Spring framework to detect when an incoming URL has an invalid parameter? It seems like the default behavior is to ignore unrecognized parameters. The best solution I can find involves adding a parameter mapping to all my endpoints and check that mapping against the parameters it is expecting.
For example, say I have a widget site with a collection endpoint.
#RequestMapping(value = "/widgets", method = RequestMethod.GET)
public ResponseEntity<WidgetList> getWidgets(
#RequestParam(value = "search", required = false) String search) {
// ...
// Get list of widgets
// ...
return new ResponseEntity<WidgetList>(widgetList, HttpStatus.OK);
}
The "search" parameter is optional because leaving it out is a convenience allowing all widgets to be found. I support a search syntax such that the following finds widgets where the foo attribute has a value of bar
GET https://example.com/widgets?search=foo:bar
A user makes a typo
GET https://example.com/widgets?saerch=foo:bar
This fails silently. Instead of finding widgets where foo=bar, all are found. I'd like it to return a 400 error stating that the "saerch" parameter is not supported. A great answer would be some sort of strict option on RequestMapping, like the following.
#RequestMapping(value = "/widgets", method = RequestMethod.GET, paramsStrict = true)
public ResponseEntity<WidgetList> getWidgets(
#RequestParam(value = "search", required = false) String search) {
// ...
// Get list of widgets
// ...
return new ResponseEntity<WidgetList>(widgetList, HttpStatus.OK);
}
As far as I know such doesn't exist. I haven't figured out a clean way to intercept the request and check for all methods (and somehow communicate which parameters are valid for each method). The best I've figured out so far is to add a parameter map and check the map against accepted parameters in every single controller method.
#RequestMapping(value = "/widgets", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<WidgetList> getWidgets(
#RequestParam(value = "search", required = false) String search,
#RequestParam Map<String, String> allRequestParams) {
validateParameters(allRequestParms);
// ...
// Get list of widgets
// ...
return new ResponseEntity<WidgetList>(widgetList, HttpStatus.OK);
}
Is there a better way to do this?
Please don't post answers about my design or how I could make the search parameter required. That's beside the point I'm trying to make with a simple example. In my real-world application there are well-designed cases where checking for invalid parameter names would be useful.
You can implement your own Servlet Filter or HandlerInterceptor to validate parameters.
Following example with Filter:
public class ParametersValidationFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (validateParameters((HttpServletRequest)request, (HttpServletResponse)response)) {
chain.doFilter(request, response);
}
}
private boolean validateParameters(HttpServletRequest request, HttpServletResponse response) {
// Check parameter names in request.getParameterNames()
/*
Invalid parameter yields response.setStatus(HttpServletReponse.SC_BAD_REQUEST)
and additional info in response body
*/
// Otherwise, validation succeeds:
return true;
}
/* Other methods */
}
Also, Filter can be configurated with init method.
This 'filter-or-interceptor' way is better due to ability to reuse as well SOLID at all.
In my JEE application, running on glassfish 3, I have the following situation:
MyFacade class
#Interceptors(SomeInterceptor.class)
public void delete(Flag somethingForTheInterceptor, String idToDelete) {
.......
}
#Interceptors(SomeInterceptor.class)
public void update(Flag somethingForTheInterceptor, MyStuff newStuff) {
.......
}
The variable somethingForTheInterceptor is not used in these methods, it is only used in the interceptor:
SomeInterceptor class
#AroundInvoke
public Object userMayAccessOutlet(InvocationContext ctx) throws Exception {
Flag flag = extractParameterOfType(Arrays.asList(ctx.getParameters()), Flag.class);
// some checks on the flag
}
Somehow it doesn't feel good to have a parameter that is not used in the method. Is there another way to "send" somethingForTheInterceptor to the interceptor?
UPDATE: The callers of delete() and update() have different ways of calculating the somethingForTheInterceptor variable. And this is not a constant. The information needed to calculate it is in the REST call. But the 2 REST methods have different input objects so it is not enough to inject the http request.
These are the callers:
MyResource class
#DELETE
#Path("/delete/{" + ID + "}")
public Response delete(#PathParam(ID) final String id) {
Flag flag = calculateFlagForInterceptor(id);
facade.delete(flag, id);
}
#POST
#Path("/update")
#Consumes(MediaType.APPLICATION_JSON + RestResourceConstants.CHARSET_UTF_8)
public Response update(final WebInputDTO updateDetails) throws ILeanException {
Flag flag = calculateFlagForInterceptor(updateDetails);
facade.update(flag, convertToMyStuff(updateDetails));
}
I was thinking - is it possible for the methods in the Resource to set the flag in some kind of Context, that can be later injected in the Interceptor?
In Java EE, Interceptors allow to add pre and post processings to a method.
So, the context of the Interceptor execution is the context of the method.
I was thinking - is it possible for the methods in the Resource to set
the flag in some kind of Context, that can be later injected in the
Interceptor?
Staless Service should be privileged when you may. So, you should avoid storing data on the server (ThreadLocal, Session, etc..).
The information needed to calculate it is
in the REST call.
Why ?
A Rest controller has no vocation to do computations and logic.
To solve your problem, are you sure you could not move the flag computation in your interceptor ?
By enhancing the interceptor responsibilities, you would have not need anly longer to transit the flag :
#AroundInvoke
public Object userMayAccessOutlet(InvocationContext ctx) throws Exception {
Flag flag = calculFlag(Arrays.asList(ctx.getParameters()));
// some checks on the flag
}