1. The controller for the User pages, and the Thymeleaf User pages themselves will use the message file for labels and messages. The updated message file is shown below. Pay particular attention to the sections labeled:
- "# USER THYMELEAF LABELS"
- "# CONTROLLER MESSAGES"
# HOME PAGE TITLES admin.page.title=Admin Home Page trader.page.title=Trader Home Page user.page.title=User Home Page login.page.title=Login Page # GENERIC BUTTON LABELS button.label.update=Update button.label.edit=Edit button.label.add=Add button.label.save=Save button.label.delete=Delete button.label.cancel=Cancel button.action.stage=stage button.action.save=save button.action.delete=delete button.action.cancel=cancel # STRATEGY THYMELEAF LABELS strategy.list.page.title=Strategy List strategy.list.head.title=Strategy List strategy.list.body.title=Strategy List strategy.list.table.title=Strategy List strategy.list.id.label=Id strategy.list.type.label=Strategy Type strategy.list.name.label=Strategy Name strategy.list.actions.label=Action strategy.list.add.title=Add strategy.delete.page.title=Delete Strategy strategy.delete.head.title=Delete Strategy strategy.edit.page.title=Edit Strategy strategy.edit.head.title=Edit Strategy # USER THYMELEAF LABELS user.list.page.title=User List user.list.table.title=User List user.list.id.label=Id user.list.user.label=Username user.list.pass.label=Password user.list.role.label=Role user.list.enabled.label=Enabled user.list.actions.label=Action user.list.add.title=Add User user.edit.page.title=Edit User user.edit.head.title=Edit User user.delete.page.title=Delete User user.delete.head.title=Delete User # CONTROLLER MESSAGES ctrl.message.error.duplicate=The {0} ''{1}'' already exists in the system. ctrl.message.error.duplicate.strategy=The {0} of type ''{1}'' with name ''{2}'' already exists in the system. ctrl.message.error.notfound=The {0} ''{1}'' does not exist in the system. ctrl.message.success.add=The {0} ''{1}'' was successfully added to the system. ctrl.message.success.update=The {0} ''{1}'' was successfully updated in the system. ctrl.message.success.cancel={0} of {1} ''{2}'' was cancelled. ctrl.message.success.delete=The {0} ''{1}'' was successfully deleted from the system.Notice the controller messages section. The numbers in brackets represent strings in an Object array that is passed to the Spring MessageSource.getMessage() method. We will look at this method in more detail when we review the controller code.
Also, to display single quotes in the message displayed to the end user, we use another single quote to escape the second single quote. So, to display one single quote we include two single quotes in the message text in the property file. Here are two articles that discuss escaping quotes in property files:
- Why Spring MessageSource arguments are not filled correctly in some locales?
- Single quote escaping in Java resource bundles
2. The controller for the User pages is shown below, with comments following this code.
package com.dtr.oas.controller; import java.util.List; import java.util.Locale; import javax.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import com.dtr.oas.exception.DuplicateUserException; import com.dtr.oas.exception.RoleNotFoundException; import com.dtr.oas.exception.UserNotFoundException; import com.dtr.oas.model.Role; import com.dtr.oas.model.User; import com.dtr.oas.service.RoleService; import com.dtr.oas.service.UserService; @Controller @RequestMapping(value = "/user") @PreAuthorize("denyAll") public class UserController { static Logger logger = LoggerFactory.getLogger(UserController.class); static String businessObject = "user"; //used in RedirectAttributes messages @Autowired private RoleService roleService; @Autowired private UserService userService; @Autowired private MessageSource messageSource; @ModelAttribute("allRoles") @PreAuthorize("hasAnyRole('CTRL_USER_LIST_GET','CTRL_USER_EDIT_GET')") public List<Role> getAllRoles() { return roleService.getRoles(); } @ModelAttribute("enabledOptions") @PreAuthorize("hasAnyRole('CTRL_USER_LIST_GET','CTRL_USER_EDIT_GET')") public boolean[] getEnabledOptions() { boolean[] array = new boolean[2]; array[0] = true; array[1] = false; return array; } @RequestMapping(value = {"/", "/list"}, method = RequestMethod.GET) @PreAuthorize("hasRole('CTRL_USER_LIST_GET')") public String listUsers(Model model) { logger.info("IN: User/list-GET"); List<User> users = userService.getUsers(); model.addAttribute("users", users); // if there was an error in /add, we do not want to overwrite // the existing user object containing the errors. if (!model.containsAttribute("userDTO")) { logger.info("Adding UserDTO object to model"); UserDTO userDTO = new UserDTO(); model.addAttribute("userDTO", userDTO); } return "user-list"; } @RequestMapping(value = "/add", method = RequestMethod.POST) @PreAuthorize("hasRole('CTRL_USER_ADD_POST')") public String addUser(@Valid @ModelAttribute UserDTO userDTO, BindingResult result, RedirectAttributes redirectAttrs) { logger.info("IN: User/add-POST"); if (result.hasErrors()) { logger.info("UserDTO add error: " + result.toString()); redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.userDTO", result); redirectAttrs.addFlashAttribute("userDTO", userDTO); return "redirect:/user/list"; } else { User user = new User(); try { user = getUser(userDTO); userService.addUser(user); String message = messageSource.getMessage("ctrl.message.success.add", new Object[] {businessObject, user.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("message", message); return "redirect:/user/list"; } catch (DuplicateUserException e) { String message = messageSource.getMessage("ctrl.message.error.duplicate", new Object[] {businessObject, userDTO.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("error", message); return "redirect:/user/list"; } catch (RoleNotFoundException rnf) { String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"role", userDTO.getRoleId()}, Locale.US); result.rejectValue("roleId", "0", message); redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.userDTO", result); redirectAttrs.addFlashAttribute("userDTO", userDTO); return "redirect:/user/list"; } } } @RequestMapping(value = "/edit", method = RequestMethod.GET) @PreAuthorize("hasRole('CTRL_USER_EDIT_GET')") public String editUserPage(@RequestParam(value = "id", required = true) Integer id, Model model, RedirectAttributes redirectAttrs) { logger.info("IN: User/edit-GET: ID to query = " + id); try { if (!model.containsAttribute("userDTO")) { logger.info("Adding userDTO object to model"); User user = userService.getUser(id); UserDTO userDTO = getUserDTO(user); logger.info("User/edit-GET: " + userDTO.toString()); model.addAttribute("userDTO", userDTO); } return "user-edit"; } catch (UserNotFoundException e) { String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"user id", id}, Locale.US); model.addAttribute("error", message); return "redirect:/user/list"; } } @RequestMapping(value = "/edit", method = RequestMethod.POST) @PreAuthorize("hasRole('CTRL_USER_EDIT_POST')") public String editUser(@Valid @ModelAttribute UserDTO userDTO, BindingResult result, RedirectAttributes redirectAttrs, @RequestParam(value = "action", required = true) String action) { logger.info("IN: User/edit-POST: " + action); if (action.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) { String message = messageSource.getMessage("ctrl.message.success.cancel", new Object[] {"Edit", businessObject, userDTO.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("message", message); } else if (result.hasErrors()) { logger.info("User-edit error: " + result.toString()); redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.userDTO", result); redirectAttrs.addFlashAttribute("userDTO", userDTO); return "redirect:/user/edit?id=" + userDTO.getId(); } else if (action.equals(messageSource.getMessage("button.action.save", null, Locale.US))) { logger.info("User/edit-POST: " + userDTO.toString()); try { User user = getUser(userDTO); userService.updateUser(user); String message = messageSource.getMessage("ctrl.message.success.update", new Object[] {businessObject, userDTO.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("message", message); } catch (DuplicateUserException unf) { String message = messageSource.getMessage("ctrl.message.error.duplicate", new Object[] {businessObject, userDTO.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("error", message); return "redirect:/user/list"; } catch (UserNotFoundException unf) { String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {businessObject, userDTO.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("error", message); return "redirect:/user/list"; } catch (RoleNotFoundException rnf) { String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"role", userDTO.getRoleId()}, Locale.US); redirectAttrs.addFlashAttribute("error", message); return "redirect:/user/list"; } } return "redirect:/user/list"; } @RequestMapping(value = "/delete", method = RequestMethod.GET) @PreAuthorize("hasRole('CTRL_USER_DELETE_GET')") public String deleteUser( @RequestParam(value = "id", required = true) Integer id, @RequestParam(value = "phase", required = true) String phase, Model model, RedirectAttributes redirectAttrs) { User user; try { user = userService.getUser(id); } catch (UserNotFoundException e) { String message = "User number [" + id + "] does not exist in the system"; redirectAttrs.addFlashAttribute("error", message); return "redirect:/user/list"; } logger.info("IN: User/delete-GET | id = " + id + " | phase = " + phase + " | " + user.toString()); if (phase.equals(messageSource.getMessage("button.action.cancel", null, Locale.US))) { String message = messageSource.getMessage("ctrl.message.success.cancel", new Object[] {"Delete", businessObject, user.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("message", message); return "redirect:/user/list"; } else if (phase.equals(messageSource.getMessage("button.action.stage", null, Locale.US))) { logger.info(" adding user: " + user.toString()); model.addAttribute("user", user); return "user-delete"; } else if (phase.equals(messageSource.getMessage("button.action.delete", null, Locale.US))) { try { userService.deleteUser(user.getId()); String message = messageSource.getMessage("ctrl.message.success.delete", new Object[] {businessObject, user.getUsername()}, Locale.US); redirectAttrs.addFlashAttribute("message", message); return "redirect:/user/list"; } catch (UserNotFoundException e) { String message = messageSource.getMessage("ctrl.message.error.notfound", new Object[] {"user id", id}, Locale.US); redirectAttrs.addFlashAttribute("error", message); return "redirect:/user/list"; } } return "redirect:/user/list"; } @PreAuthorize("hasAnyRole('CTRL_USER_EDIT_GET','CTRL_USER_DELETE_GET')") public UserDTO getUserDTO(User user) { UserDTO userDTO = new UserDTO(); userDTO.setId(user.getId()); userDTO.setUsername(user.getUsername()); userDTO.setPassword(user.getPassword()); userDTO.setEnabled(user.getEnabled()); userDTO.setRoleId(user.getRole().getId()); return userDTO; } @PreAuthorize("hasAnyRole('CTRL_USER_ADD_POST','CTRL_USER_EDIT_POST')") public User getUser(UserDTO userDTO) throws RoleNotFoundException { User user = new User(); user.setId(userDTO.getId()); Role role = roleService.getRole(userDTO.getRoleId()); user.setRole(role); user.setUsername(userDTO.getUsername()); user.setPassword(userDTO.getPassword()); user.setEnabled(userDTO.getEnabled()); return user; } }There are several points that I will discuss below. The associated lines are highlighted in the code above.
- There is an instance variable named businessObject. This string is used in the messages returned from the controller and this variable allows us to reuse messages in the messages_en.properties file across controllers.
- The @Autowired MessageSource variable allows us to access messages in the messages_en.properties file, and is used throughout the controller.
- All of the methods in the controller are annotated with @PreAuthorize. These tags contain permission values for fine grained authorization. We set up the infrastructure to use permissions rather than roles for authorization in the last series of posts.
- There are two methods that are annotated with @ModelAttributes. These methods will populate the response with lists used to populate dropdown lists on the User Thymeleaf pages.
- Also notice the call to the roleService to get all of the roles in the system.
- Some of the methods in the controller that are annotated with @RequestMapping get/set a User DTO rather than a User Entity. We have to use this DTO object to pass roleIds to/from a form. Unfortunately, we can't assign Role objects in a form...as far as I know, so we need to use a new object for this purpose. The list page uses Entities for the list, but a DTO for the add section. The edit page uses a DTO, and the delete page uses an Entity. You will see Thymeleaf examples in the next post.
- In the addUser() method you will notice the call to messageSource.getMessage(), that we touched on in bullet one in this post. The parameters are:
- The key from the messages_en.properties file.
- An object array containing the arguments that will be substituted for the {#} placeholders in the messages in the messages_en.properties file.
- The Locale.
- I add the message as a flash attribute with the identifier of either "message" or "error". These attributes will be displayed on the Thymeleaf pages in a dedicated message display section.
- Finally, note the two helper methods at the bottom of the controller that convert from DTO to Entity, and from Entity to DTO.
3. The User DTO that we created contains the same validations contained in the User Entity. I don't like the redundancy, but I couldn't find an elegant solution. Also, you will notice that the DTO contains a roleId instance variable of type int, rather than the role instance variable of type Role contained in the Entity. The DTO code is shown below.
package com.dtr.oas.controller; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.NotEmpty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Objects; public class UserDTO implements Serializable { private static final long serialVersionUID = -1631797874369735181L; static Logger logger = LoggerFactory.getLogger(UserDTO.class); private int id; @NotNull(message = "{error.user.username.null}") @NotEmpty(message = "{error.user.username.empty}") @Size(max = 50, message = "{error.user.username.max}") private String username; @NotNull(message = "{error.user.password.null}") @NotEmpty(message = "{error.user.password.empty}") @Size(max = 50, message = "{error.user.password.max}") private String password; private boolean enabled; private int roleId; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean getEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public int getRoleId() { return roleId; } public void setRoleId(int roleId) { this.roleId = roleId; } @Override public String toString() { return String.format("%s(id=%d, username=%s, password=%s, enabled=%b, roleID=%d)", this.getClass().getSimpleName(), this.getId(), this.getUsername(), this.getPassword(), this.getEnabled(), this.getRoleId()); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null) return false; if (o instanceof UserDTO) { final UserDTO other = (UserDTO) o; return Objects.equal(getId(), other.getId()) && Objects.equal(getUsername(), other.getUsername()) && Objects.equal(getPassword(), other.getPassword()) && Objects.equal(getEnabled(), other.getEnabled()) && Objects.equal(getRoleId(), other.getRoleId()); } return false; } @Override public int hashCode() { return Objects.hashCode(getId(), getUsername(), getPassword(), getEnabled(), getRoleId()); } }
In the next post we will create the Thymeleaf CRUD pages, update the menus, and update the Twitter Bootstrap functionality (associated with alerts and actions menus).
1 comment:
Post a Comment