I've been trying to figure out what the best practice is for form submission with spring and what the minimum boilerplate is to achieve that.
I think of the following as best practise traits
Validation enabled and form values preserved on validation failure
Disable form re-submission F5 (i.e. use redirects)
Prevent the model values to appear in the URL between redirects (model.clear())
So far I've come up with this.
#Controller
#RequestMapping("/")
public class MyModelController {
#ModelAttribute("myModel")
public MyModel myModel() {
return new MyModel();
}
#GetMapping
public String showPage() {
return "thepage";
}
#PostMapping
public String doAction(
#Valid #ModelAttribute("myModel") MyModel myModel,
BindingResult bindingResult,
Map<String, Object> model,
RedirectAttributes redirectAttrs) throws Exception {
model.clear();
if (bindingResult.hasErrors()) {
redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.myModel", bindingResult);
redirectAttrs.addFlashAttribute("myModel", myModel);
} else {
// service logic
}
return "redirect:/thepage";
}
}
Is there a way to do this with less boilerplate code or is this the least amount of code required to achieve this?
First, I wouldn't violate the Post/Redirect/Get (PRG) pattern, meaning I would only redirect if the form is posted successfully.
Second, I would get rid of the BindingResult style altogether. It is fine for simple cases, but once you need more complex notifications to reach the user from service/domain/business logic, things get hairy. Also, your services are not much reusable.
What I would do is pass the bound DTO directly to the service, which would validate the DTO and put a notification in case of errors/warning. This way you can combine business logic validation with JSR 303: Bean Validation.
For that, you can use the Notification Pattern in the service.
Following the Notification Pattern, you would need a generic notification wrapper:
public class Notification<T> {
private List<String> errors = new ArrayList<>();
private T model; // model for which the notifications apply
public Notification<T> pushError(String message) {
this.errors.add(message);
return this;
}
public boolean hasErrors() {
return !this.errors.isEmpty();
}
public void clearErrors() {
this.errors.clear();
}
public String getFirstError() {
if (!hasErrors()) {
return "";
}
return errors.get(0);
}
public List<String> getAllErrors() {
return this.errors;
}
public T getModel() {
return model;
}
public void setModel(T model) {
this.model = model;
}
}
Your service would be something like:
public Notification<MyModel> addMyModel(MyModelDTO myModelDTO){
Notification<MyModel> notification = new Notification();
//if(JSR 303 bean validation errors) -> notification.pushError(...); return notification;
//if(business logic violations) -> notification.pushError(...); return notification;
return notification;
}
And then your controller would be something like:
Notification<MyModel> addAction = service.addMyModel(myModelDTO);
if (addAction.hasErrors()) {
model.addAttribute("myModel", addAction.getModel());
model.addAttribute("notifications", addAction.getAllErrors());
return "myModelView"; // no redirect if errors
}
redirectAttrs.addFlashAttribute("success", "My Model was added successfully");
return "redirect:/thepage";
Although the hasErrors() check is still there, this solution is more extensible as your service can continue evolving with new business rules notifications.
Another approach which I will keep very short, is to throw a custom RuntimeException from your services, this custom RuntimeException can contain the necessary messages/models, and use #ControllerAdvice to catch this generic exception, extract the models and messages from the exception and put them in the model. This way, your controller does nothing but forward the bound DTO to service.
Based on the answer by #isah, if redirect happens only after successful validation the code can be simplified to this:
#Controller
#RequestMapping("/")
public class MyModelController {
#ModelAttribute("myModel")
public MyModel myModel() {
return new MyModel();
}
#GetMapping
public String showPage() {
return "thepage";
}
#PostMapping
public String doAction(
#Valid #ModelAttribute("myModel") MyModel myModel,
BindingResult bindingResult,
RedirectAttributes redirectAttrs) throws Exception {
if (bindingResult.hasErrors()) {
return "thepage";
}
// service logic
redirectAttrs.addFlashAttribute("success", "My Model was added successfully");
return "redirect:/thepage";
}
}
One possible way is to use Archetype for Web forms, Instead of creating simple project, you can choose to create project from existing archetype of web forms. It will provide you with sufficient broiler plate code. You can also make your own archetype.
Have a look at this link to get deeper insight into archetypes.
Link To Archetypes in Java Spring
Related
So I was reading about EventListener (beforeSave, afterSave, etc) here, when it comes to validation, and I can't really figure it out why it is used and how.
For example, for my own app, for an entity named curse, I have a method validateCursa() included in curseService class that validate the details of a curse object and throws custom exceptions.
example:
CurseController:
#PostMapping(path="/add") // Map ONLY POST Requests
public #ResponseBody String addNewCursa (#Valid #RequestBody curse C2,
BindingResult bindingResult) {
com.example.demo.models.curseService.validateCursa(C2);
if(bindingResult.hasErrors()) {
return "error";
}
curseService.addCursa(C2);
return "Saved";
}
curseService:
public static void validateCursa(curse c){
some validation here
}
public curse addCursa(curse c){
return curseRepository.save(c);
}
So my question is: What really does that EventListener/validation do, when should it be used and why? Like, should I re-design my validation process and use these EventListeners?
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
}
I'm using Spring Boot with Data JPA.
I have the following code.
A User Class with name and an informative message.
class UserResponse{
private String name;
private String message;
}
User JPA Repository which finds userBy id;
interface UserRepository{
Optional<User> findUserById(String id);
}
User Service which invokes repo and set message if user not found
class UserService(){
UserResponse user = new UserResponse();
public UserResponse getUserById(String userId){
Optional<User> useroptional = userRepository.findById(userId);
if(userOptional.isPresent()){
user.setName(userOptional.get().getName());
}else{
user.setMessage("User Not Found");
}
}
UserController has to set proper HTTP status code as per the message.
class UserController(){
public ResponseEntity<UserResponse> getUserById(String id){
UserResponse user = userService.getUserById(id);
HttpStatus status = OK;
if(!StringUtils.isEmpty(user.getMessage())){
status = NOT_FOUND;
}
return new ResponseEntity<>(user,status);
}
}
The problems I have faced is inorder to set proper status code in controller layer I have to inspect user message,which i didn't like.
Is there anyway we can create a control flow for Success and Failure cases.
Say One Return type and flow for Success scenario and vice-versa.
I know Scala has this feature with Either keyword.
Is there any alternate in Java ?
Or any other approach I can use to handle this better...
One approach would be returning RepsonseEntity in service layer itself with proper status code but setting status code is controller's Responsibility is what I felt.
In case of failure you can throw custom Exception with proper message. Then you can catch it in #ControllerAdvice. I'll add an example in a moment.
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(MyCustomException.class)
public ResponseEntity<String> exception(MyCustomException e) {
return new ResponseEntity(e.getMessage(), HttpStatus.NotFound);
}
}
In one #ControllerAdvice one could have more methods listening for different Exceptions. Custom Exception can hold whatever you want - it's a normal class - so you can return ResponseEntity of whatever you want.
For example:
#Transactional(readOnly = true)
#GetMapping("/{id}")
public ResponseEntity<?> getUserById(#PathVariable("id") String userId) {
return userRepository.findById(userId)
.map(user -> ResponseEntity.ok().body(user))
.orElse(new ResponseEntity<>(/* new ErrorMessage */, HttpStatus.NOT_FOUND))
}
For 'not found' response you have to create an error message object and return it to client.
Use Case:
let's design a RESTful create operation using POST HTTP verb - creating tickets where creator (assigner) specifies a ticket assignee
we're creating a new "ticket" on following location: /companyId/userId/ticket
we're providing ticket body containing assigneeId:
{
"assigneeId": 10
}
we need to validate that assigneeId belongs to company in URL - companyId path variable
So far:
#RequestMapping(value="/{companyId}/{userId}/ticket", method=POST)
public void createTicket(#Valid #RequestBody Ticket newTicket, #PathVariable Long companyId, #PathVariable Long userId) {
...
}
we can easily specify a custom Validator (TicketValidator) (even with dependencies) and validate Ticket instance
we can't easily pass companyId to this validator though! We need to verify that ticket.assigneeId belongs to company with companyId.
Desired output:
ability to access path variables in custom Validators
Any ideas how do I achieve the desired output here?
If we assume that our custom validator knows desired property name, then we can do something like this:
Approach one:
1) We can move this getting path variables logic to some kind of a base validator:
public abstract class BaseValidator implements Validator {
#Override
public boolean supports(Class<?> clazz)
{
// supports logic
}
#Override
public void validate(Object target, Errors errors)
{
// some base validation logic or empty if there isn't any
}
protected String getPathVariable(String name) {
// Getting current request (Can be autowired - depends on your implementation)
HttpServletRequest req = HttpServletRequest((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
if (req != null) {
// getting variables map from current request
Map<String, String> variables = req.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
return variables.get(name);
}
return null;
}
}
2) Extend it with your TicketValidator implementation:
public class TicketValidator extends BaseValidator {
#Override
public void validate(Object target, Errors errors)
{
// Getting our companyId var
String companyId = getPathVariable("companyId");
...
// proceed with your validation logic. Note, that all path variables
// is `String`, so you're going to have to cast them (you can do
// this in `BaseValidator` though, by passing `Class` to which you
// want to cast it as a method param). You can also get `null` from
// `getPathVariable` method - you might want to handle it too somehow
}
}
Approach two:
I think it worth to mention that you can use #PreAuthorize annotation with SpEL to do this kind of validation (You can pass path variables and request body to it). You'll be getting HTTP 403 code though if validation woudnt pass, so I guess it's not exaclty what you want.
You could always do this:
#Controller
public class MyController {
#Autowired
private TicketValidator ticketValidator;
#RequestMapping(value="/{companyId}/{userId}/ticket", method=POST)
public void createTicket(#RequestBody Ticket newTicket,
#PathVariable Long companyId, #PathVariable Long userId) {
ticketValidator.validate(newTicket, companyId, userId);
// do whatever
}
}
Edit in response to the comment:
It doesn't make sense to validate Ticket independently of companyId when the validity of Ticket depends on companyId.
If you cannot use the solution above, consider grouping Ticket with companyId in a DTO, and changing the mapping like this:
#Controller
public class MyController {
#RequestMapping(value="/{userId}/ticket", method=POST)
public void createTicket(#Valid #RequestBody TicketDTO ticketDto,
#PathVariable Long userId) {
// do whatever
}
}
public class TicketDTO {
private Ticket ticket;
private Long companyId;
// setters & getters
}
I like to implement a REST-API into my SpringMVC application. At the moment, I have one method to handle POST-Requests, which "returns" a rendered ViewScript.
#RequestMapping(method=RequestMethod.POST)
public String onSubmit(User user, Model model)
{
return "success";
}
It would be nice, to add a second method with the #ResponseBody Annotation for POST-Requests, e.g. to send a JSON-Response.
Furthermore, the old Method still has to exists, to handle "normal" Requests.
But a code like this doesn't work:
#RequestMapping(method=RequestMethod.POST)
public String onSubmit(User user, Model model)
{
return "success";
}
#RequestMapping(method=RequestMethod.POST)
#ResponseBody
public Object add(User user, Model model)
{
// [...]
return myObject;
}
With this code, I'm getting a 405 (Method Not Allowed) Error from Tomcat. How can I fix this?
As it stands, Spring has no way to differentiate between these two requests: same URL, same request method.
You can further differentiate by mimetype:
#RequestMapping(method=RequestMethod.POST, headers="content-type=application/json")
Although there are several mimetypes associated with JSON :/ The headers value takes an array, however, so you can narrow/widen it as necessary.
See the headers docs.
Dont USE TWO ANNOTATION. It is a poor option. Just have one more method without annotation. But the method from the old method by checking the below condition.
JUST PASS ONE MORE ARGUMENT FROM UI by query parameter(request="JSON_Request").
#RequestMapping(method=RequestMethod.POST)
public String onSubmit(User user, Model model)
{
if(request="JSON_Request") {
newMethod(user, model);
}
return "success";
}
private Object newMethod(User user, Model model)
{
// [...]
return myObject;
}