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);
`
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.
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.
I tried binding N numbers of parameters in a URL like this
/web/appl/<applname>/rule/<rulename>/<attrname1>/<attrval1>/<attrname2>/<attrval2>/.../<attrnameN>/<attrvalN>
with
#RequestMapping(value = "/web/appl/{applname}/rule/{rulename}/{attributes}", method = RequestMethod.GET)
public Object GetService(HttpServletRequest request, #PathVariable("attributes") Map<String, Object> map,
#PathVariable("applname") String applname, #PathVariable("rulename") String rulename)
throws Exception {
...
}
but could not get values of <attrval1>/<attrname2>/<attrval2>/.../<attrnameN>/<attrvalN>
Unfortunately Spring MVC does not provide such a solution. You can consider the Matrix Variables as an alternative.
If you prefer sticking to your current URI scheme then you have to implement a solution yourself. One approach is to use a path pattern. Example:
#RequestMapping(value = "/web/appl/{applname}/rule/{rule name}/**")
public Object getService(HttpServletRequest request,
#PathVariable("applname") String applname ...) {
String attributesPart = new AntPathMatcher()
.extractPathWithinPattern("/web/appl/{applname}/rule/{rule name}/**",
request.getServletPath());
...
You could implement your argument resolver that does that. Something like
#RequestMapping(value = "/web/appl/{applname}/rule/{rule name}/**")
public Object getService(#MyAttributes Map<String, String> attributes,
#PathVariable("applname") String applname ...) {
Use
String urlAttributes = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
and get whole URL. After that parse this URL according to your needs like if you did not get
<attrval1>/<attrname2>/<attrval2>/.../<attrnameN>/<attrvalN>
in proper sequence then you can throw exception.
I am trying to reload the same page with different content that varies depending on which link is clicked on the page. The url pattern for the page is "/owners", which triggers the running of this method in OwnerController.java:
#RequestMapping(value = "/owners", method = RequestMethod.GET)
public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) {
Collection<Owner> results = this.clinicService.findOwnerByLastName("");
model.put("selections", results);
model.put("sel_owner",this.clinicService.findOwnerById(ownerId));//ownerId is not defined yet
return "owners/ownersList";
}
The jsp includes a dynamically generated list of ownerId integer values, each of which can be used to send a unique ownerId back to the server. What do I need to add to my jsp in order to get ownerId to have a user-specified value when processFindForm() is run? In other words, what does the hyperlink need to look like?
You are using GET request type.If you want to send any parameter to controller then you have to use either #RequestParam or #PathParam annotations based on requirement as an arguments to controller method. In your case it will be something like...
public String processFindForm(#RequestParam("ownerID") String ownerId, BindingResult result, Map<String, Object> model) {... }
Take a look on this link as well: http://www.byteslounge.com/tutorials/spring-mvc-requestmapping-example
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).