/** * Copyright (C) 2011 JTalks.org Team * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jtalks.jcommune.web.controller; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Language; import org.jtalks.jcommune.model.entity.Post; import org.jtalks.jcommune.service.PostService; import org.jtalks.jcommune.service.UserContactsService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.nontransactional.ImageConverter; import org.jtalks.jcommune.service.nontransactional.ImageService; import org.jtalks.jcommune.web.dto.*; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.jtalks.jcommune.web.validation.editors.DefaultAvatarEditor; import org.jtalks.jcommune.web.validation.editors.DefaultStringEditor; import org.jtalks.jcommune.web.validation.editors.PageSizeEditor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.support.RequestContextUtils; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.util.Locale; /** * Controller for User related actions: registration, user profile operations and so on. * * @author Kirill Afonin * @author Alexandre Teterin * @author Max Malakhov * @author Eugeny Batov * @author Evgeniy Naumenko * @author Anuar_Nurmakanov * @author Andrey Pogorelov * @author Andrey Ivanov */ @Controller public class UserProfileController { /** * We need this properties for determining * the desired operation while saving user */ public static final String SECURITY = "security"; public static final String PROFILE = "profile"; public static final String NOTIFICATIONS = "notifications"; public static final String CONTACTS = "contacts"; public static final String EDIT_PROFILE = "editProfile"; public static final String EDITED_USER = "editedUser"; public static final String BREADCRUMB_LIST = "breadcrumbList"; private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); public static final String IS_PASSWORD_CHANGED_ATTRIB = "isPasswordChangedMessage"; private ImageService imageService; private UserService userService; private BreadcrumbBuilder breadcrumbBuilder; private ImageConverter imageConverter; private PostService postService; private UserContactsService contactsService; private EntityToDtoConverter converter; private RetryTemplate retryTemplate; /** * This method turns the trim binder on. Trim binder * removes leading and trailing spaces from the submitted fields. * So, it ensures, that all validations will be applied to * trimmed field values only. * <p/> There is no need for trim edit password fields, * so they are processed with {@link org.jtalks.jcommune.web.validation.editors.DefaultStringEditor} * * @param binder Binder object to be injected */ @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(String.class, "userSecurityDto.currentUserPassword", new DefaultStringEditor(true)); binder.registerCustomEditor(String.class, "userSecurityDto.newUserPassword", new DefaultStringEditor(true)); binder.registerCustomEditor(String.class, "userSecurityDto.newUserPasswordConfirm", new DefaultStringEditor(true)); binder.registerCustomEditor(Integer.class, "userProfileDto.pageSize", new PageSizeEditor()); binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); binder.registerCustomEditor(String.class, "avatar", new DefaultAvatarEditor(imageService)); } /** * @param userService to get current user and user by id * @param breadcrumbBuilder the object which provides actions on {@link org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder} entity * @param imageConverter to prepare user avatar for view * @param postService to get all user's posts * @param contactsService for edit user contacts * @param imageService for DefaultAvatarEditor */ @Autowired public UserProfileController(UserService userService, BreadcrumbBuilder breadcrumbBuilder, @Qualifier("avatarPreprocessor") ImageConverter imageConverter, PostService postService, UserContactsService contactsService, @Qualifier("avatarService") ImageService imageService, EntityToDtoConverter converter, RetryTemplate retryTemplate) { this.userService = userService; this.breadcrumbBuilder = breadcrumbBuilder; this.imageConverter = imageConverter; this.postService = postService; this.contactsService = contactsService; this.imageService = imageService; this.converter = converter; this.retryTemplate = retryTemplate; } /** * This method is a shortcut for user profile access. It may be usefull when we haven't got * the specific id, but simply want to access current user's profile. * <p/> * Requires user to be authorized. * * @return user details view with {@link org.jtalks.jcommune.model.entity.JCUser} object. */ @RequestMapping(value = "/user", method = RequestMethod.GET) public ModelAndView showCurrentUserProfilePage() { JCUser user = userService.getCurrentUser(); return getUserProfileModelAndView(user, PROFILE); } /** * Formats model and view for representing user's details * * @param user user * @param settingsType type of user settings (profile, contacts, security or notifications) * @return user's details */ private ModelAndView getUserProfileModelAndView(JCUser user, String settingsType) { EditUserProfileDto editedUserDto; switch (settingsType) { case CONTACTS: editedUserDto = new EditUserProfileDto(new UserContactsDto(user), user); editedUserDto.getUserContactsDto().setContactTypes(contactsService.getAvailableContactTypes()); break; case NOTIFICATIONS: editedUserDto = new EditUserProfileDto(new UserNotificationsDto(user), user); break; case SECURITY: editedUserDto = new EditUserProfileDto(new UserSecurityDto(user), user); break; default: editedUserDto = new EditUserProfileDto(new UserProfileDto(user), user); } setAvatarToUserProfileView(editedUserDto, user); return new ModelAndView(EDIT_PROFILE, EDITED_USER, editedUserDto); } /** * Show user profile page for specified user. * * @return edit user profile page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * throws if current logged in user was not found */ @RequestMapping(value = {"/users/{editedUserId}/profile", "/users/{editedUserId}"}, method = RequestMethod.GET) public ModelAndView showUserProfile(@PathVariable Long editedUserId) throws NotFoundException { JCUser editedUser = userService.get(editedUserId); return getUserProfileModelAndView(editedUser, PROFILE); } /** * Show user contacts page for specified user. * * @return edit user contacts page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * throws if current logged in user was not found */ @RequestMapping(value = "/users/{editedUserId}/contacts", method = RequestMethod.GET) public ModelAndView showUserContacts(@PathVariable Long editedUserId) throws NotFoundException { JCUser editedUser = userService.get(editedUserId); return getUserProfileModelAndView(editedUser, CONTACTS); } /** * Show user notifications page for specified user. * * @return edit user notifications page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * throws if current logged in user was not found */ @RequestMapping(value = "/users/{editedUserId}/notifications", method = RequestMethod.GET) public ModelAndView showUserNotificationSettings(@PathVariable Long editedUserId) throws NotFoundException { checkPermissionForEditNotificationsOrSecurity(editedUserId); JCUser editedUser = userService.get(editedUserId); return getUserProfileModelAndView(editedUser, NOTIFICATIONS); } /** * Show user security page for specified user. * * @return edit user security page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * throws if current logged in user was not found */ @RequestMapping(value = "/users/{editedUserId}/security", method = RequestMethod.GET) public ModelAndView showUserSecuritySettings(@PathVariable Long editedUserId) throws NotFoundException { checkPermissionForEditNotificationsOrSecurity(editedUserId); JCUser editedUser = userService.get(editedUserId); return getUserProfileModelAndView(editedUser, SECURITY); } /** * Set avatar to data transfer object for view. * * @param user passed user */ private void setAvatarToUserProfileView(EditUserProfileDto editUserProfileDto, JCUser user) { byte[] avatar = user.getAvatar(); editUserProfileDto.setAvatar(imageConverter.prepareHtmlImgSrc(avatar)); } /** * Update user profile info. Check if the user enter valid data and update profile in database. * In error case return into the edit profile page and draw the error. * <p/> * * @param editedProfileDto dto populated by user * @param result binding result which contains the validation result * @param response http servlet response * @return return to user profile page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * if edited user doesn't exist in system */ @RequestMapping(value = "/users/*/profile", method = RequestMethod.POST) public ModelAndView saveEditedProfile(@Valid @ModelAttribute(EDITED_USER) EditUserProfileDto editedProfileDto, BindingResult result, HttpServletResponse response) throws NotFoundException { if (result.hasErrors()) { return new ModelAndView(EDIT_PROFILE, EDITED_USER, editedProfileDto); } long editedUserId = editedProfileDto.getUserProfileDto().getUserId(); checkPermissionsToEditProfile(editedUserId); JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, PROFILE)); //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + PROFILE); } /** * Update user notification settings. Check if the user enter valid data and update settings in database. * In error case return into the edit profile page and draw the error. * <p/> * * @param editedProfileDto dto populated by user * @param result binding result which contains the validation result * @param response http servlet response * @return in case of errors return back to edit notifications page, in another case return to user profile page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * if edited user doesn't exist in system */ @RequestMapping(value = "/users/*/notifications", method = RequestMethod.POST) public ModelAndView saveEditedNotifications(@Valid @ModelAttribute(EDITED_USER) EditUserProfileDto editedProfileDto, BindingResult result, HttpServletResponse response) throws NotFoundException { if (result.hasErrors()) { return new ModelAndView(EDIT_PROFILE, EDITED_USER, editedProfileDto); } long editedUserId = editedProfileDto.getUserNotificationsDto().getUserId(); checkPermissionForEditNotificationsOrSecurity(editedUserId); JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, NOTIFICATIONS)); //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + NOTIFICATIONS); } /** * Update user security info. Check if the user enter valid data and update user security info in database. * In error case return into the edit profile page and draw the error. * <p/> * * @param editedProfileDto dto populated by user * @param result binding result which contains the validation result * @param response http servlet response * @return in case of errors return back to edit security page, in another case return to user profile page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * if edited user doesn't exist in system */ @RequestMapping(value = "/users/*/security", method = RequestMethod.POST) public ModelAndView saveEditedSecurity(@Valid @ModelAttribute(EDITED_USER) EditUserProfileDto editedProfileDto, BindingResult result, HttpServletResponse response, RedirectAttributes redirectAttributes) throws NotFoundException { if (result.hasErrors()) { return new ModelAndView(EDIT_PROFILE, EDITED_USER, editedProfileDto); } long editedUserId = editedProfileDto.getUserSecurityDto().getUserId(); checkPermissionForEditNotificationsOrSecurity(editedUserId); JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, SECURITY)); if (editedProfileDto.getUserSecurityDto().getNewUserPassword() != null) { redirectAttributes.addFlashAttribute(IS_PASSWORD_CHANGED_ATTRIB, true); } //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + SECURITY); } /** * Update user contacts. Check if the user enter valid data and update contacts in database. * In error case return into the edit profile page and draw the error. * <p/> * * @param editedProfileDto dto populated by user * @param result binding result which contains the validation result * @param response http servlet response * @return in case of errors return back to edit contacts page, in another case return to user profile page * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * if edited user doesn't exist in system */ @RequestMapping(value = "/users/*/contacts", method = RequestMethod.POST) public ModelAndView saveEditedContacts(@Valid @ModelAttribute(EDITED_USER) EditUserProfileDto editedProfileDto, BindingResult result, HttpServletResponse response) throws NotFoundException { if (result.hasErrors()) { editedProfileDto.getUserContactsDto().setContactTypes(contactsService.getAvailableContactTypes()); return new ModelAndView(EDIT_PROFILE, EDITED_USER, editedProfileDto); } long editedUserId = editedProfileDto.getUserId(); checkPermissionsToEditProfile(editedUserId); JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, CONTACTS)); //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + CONTACTS); } /** * User doesn't need to have permission to edit his password and notifications. * For other users we have to check permission to edit other profiles. * * @param editedUserId an identifier of edited user * @see <a href="http://jira.jtalks.org/browse/JC-1740">JC-1740</a> */ private void checkPermissionForEditNotificationsOrSecurity(long editedUserId) { JCUser editorUser = userService.getCurrentUser(); if (editorUser.getId() != editedUserId) { userService.checkPermissionToEditOtherProfiles(editorUser.getId()); } } /** * User must have permissions to edit own or other profiles. * So we must check them for users, who try to edit profiles. * * @param editedUserId an identifier of edited user */ private void checkPermissionsToEditProfile(long editedUserId) { JCUser editorUser = userService.getCurrentUser(); if (editorUser.getId() == editedUserId) { userService.checkPermissionToEditOwnProfile(editorUser.getId()); } else { userService.checkPermissionToEditOtherProfiles(editorUser.getId()); } } /** * Show page with post of user. * SpEL pattern in a var name indicates we want to consume all the symbols in a var, * even dots, which Spring MVC uses as file extension delimiters by default. * * @param page number current page * @param id database user identifier * @return post list of user * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * if user with given id not found. */ @RequestMapping(value = "/users/{id}/postList", method = RequestMethod.GET) public ModelAndView showUserPostList(@PathVariable Long id, @RequestParam(value = "page", defaultValue = "1", required = false) String page) throws NotFoundException { JCUser user = userService.get(id); Page<Post> postsPage = postService.getPostsOfUser(user, page); return new ModelAndView("userPostList") .addObject("user", user) .addObject("postsPage", converter.convertPostPageToPostDtoPage(postsPage)) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb()); } @RequestMapping(value = "**/language", method = RequestMethod.GET) public String saveUserLanguage(@RequestParam(value = "lang", defaultValue = "en") String lang, HttpServletResponse response, HttpServletRequest request) throws ServletException { final JCUser jcuser = userService.getCurrentUser(); final Language languageFromRequest = Language.byLocale(new Locale(lang)); if (!jcuser.isAnonymous()) { retryTemplate.execute(new RetryCallback<Void, RuntimeException>() { @Override public Void doWithRetry(RetryContext context) throws RuntimeException { userService.changeLanguage(jcuser, languageFromRequest); return null; } }); } LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); localeResolver.setLocale(request, response, languageFromRequest.getLocale()); return "redirect:" + request.getHeader("Referer"); } /** * Save user profile settings depending on settings type. * * @param userId user Id * @param userProfileDto dto with user settings * @param settingsType user settings type * @return updated user * @throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException * */ private JCUser saveUserData(long userId, EditUserProfileDto userProfileDto, String settingsType) throws NotFoundException { switch (settingsType) { case SECURITY: return userService.saveEditedUserSecurity(userId, userProfileDto.getUserSecurityContainer()); case NOTIFICATIONS: return userService.saveEditedUserNotifications(userId, userProfileDto.getUserNotificationsContainer()); case CONTACTS: return contactsService.saveEditedUserContacts(userId, userProfileDto.getUserContacts()); default: return userService.saveEditedUserProfile(userId, userProfileDto.getUserInfoContainer()); } } private class SaveProfileRetryCallback implements RetryCallback<JCUser, NotFoundException> { private long editedUserId; private EditUserProfileDto editedProfileDto; private String settingsType; public SaveProfileRetryCallback(long editedUserId, EditUserProfileDto editedProfileDto, String settingsType) { this.editedUserId = editedUserId; this.editedProfileDto = editedProfileDto; this.settingsType = settingsType; } @Override public JCUser doWithRetry(RetryContext context) throws NotFoundException { return saveUserData(editedUserId, editedProfileDto, settingsType); } } }