From d7c4482200b78e6c0dba97c5a3df0f3b723c12c0 Mon Sep 17 00:00:00 2001 From: Denis Natusch Date: Wed, 22 Nov 2023 00:05:03 +0100 Subject: [PATCH] Add form validator to users package Closes #35 Closes #59 Closes #64 Closes #71 Co-authored-by: Simon Bruder --- .../asciidoc/developer_documentation.adoc | 3 +- .../asciidoc/models/design/seq_users.puml | 31 +++--- src/main/asciidoc/models/design/seq_users.svg | 4 +- src/main/asciidoc/models/design/user.puml | 44 ++++++--- src/main/asciidoc/models/design/user.svg | 4 +- .../java/catering/users/FormValidator.java | 25 +++++ src/main/java/catering/users/ProfileForm.java | 38 -------- .../java/catering/users/RegistrationForm.java | 36 ------- .../java/catering/users/UserController.java | 39 +++++--- src/main/java/catering/users/UserForm.java | 97 +++++++++++++++++++ .../java/catering/users/UserManagement.java | 4 + src/main/resources/templates/profile.html | 18 ++-- src/main/resources/templates/register.html | 14 ++- 13 files changed, 228 insertions(+), 129 deletions(-) create mode 100644 src/main/java/catering/users/FormValidator.java delete mode 100644 src/main/java/catering/users/ProfileForm.java delete mode 100644 src/main/java/catering/users/RegistrationForm.java create mode 100644 src/main/java/catering/users/UserForm.java diff --git a/src/main/asciidoc/developer_documentation.adoc b/src/main/asciidoc/developer_documentation.adoc index dd488d8..e5bc32f 100644 --- a/src/main/asciidoc/developer_documentation.adoc +++ b/src/main/asciidoc/developer_documentation.adoc @@ -285,7 +285,8 @@ image:models/design/user.svg[class design diagram - User] |UserManagement |A class that manages the UserRepository. |UserRepository |An extension of 'CrudRepository' to store Users. |User |A class that allows a person to associate system data with themselves. -|RegistrationForm |A Form to cache a user input that was made while registration. +|UserForm |A Form to cache a user input that was made during registration or updating the profile. +|FormValidator |A validator that validates the UserForm. |=== === Staff diff --git a/src/main/asciidoc/models/design/seq_users.puml b/src/main/asciidoc/models/design/seq_users.puml index dd4c669..6eb2442 100644 --- a/src/main/asciidoc/models/design/seq_users.puml +++ b/src/main/asciidoc/models/design/seq_users.puml @@ -38,7 +38,9 @@ UserController -> RegistrationForm : getPassword() UserController <-- RegistrationForm : String UserController -> RegistrationForm : getAddress() UserController <-- RegistrationForm : String -UserController -> UserManagement : createCustomer(String,String,String) +UserController -> RegistrationForm : getFullName() +UserController <-- RegistrationForm : String +UserController -> UserManagement : createCustomer(String,String,String,String) activate UserManagement UserManagement -> UserRepository : "save(User:customer)" activate User_customer @@ -55,7 +57,7 @@ deactivate Spring == Disable Customer by Customer == -Spring -> UserController : disableUser() +Spring -> UserController : disableUser(UserAccount:userAccount) activate User_customer activate Spring activate UserController @@ -88,9 +90,15 @@ UserManagement -> UserRepository : findAll() activate UserRepository UserManagement <-- UserRepository : Streamble deactivate UserRepository -UserController <-- UserManagement : Optional +UserController <-- UserManagement : Optional +UserController -> User : getUsername() +UserController <-- User : String +UserController -> User : getFullName() +UserController <-- User : String +UserController -> User : getAddress() +UserController <-- User : String deactivate UserManagement -UserController -> Spring : model.addAttribute("user",User:user) +UserController -> Spring : model.addAttribute("profileForm",ProfileForm) UserController <-- Spring : Model Spring <-- UserController : "profile" deactivate Spring @@ -99,7 +107,7 @@ deactivate User == Edit Profile == -Spring -> UserController : editProfile(UserAccount:LoggedIn,String:password,String:address,String:username) +Spring -> UserController : editProfile(UserAccount:LoggedIn,ProfileForm:form,Erros:result,Model:model) activate User activate Spring activate UserController @@ -111,23 +119,20 @@ UserManagement <-- UserRepository : Streamble deactivate UserRepository UserController <-- UserManagement : Optional deactivate UserManagement -UserController -> User : [!String:username.isBlank()] setUsername(String:username) +UserController -> User : setUsername(String:username) UserController <-- User : boolean -UserController -> User : [!String:address.isBlank()] setAddress(String:address) +UserController -> User : setAddress(String:address) UserController <-- User : boolean -UserController -> User : [!String:password.isBlank()] setPassword(String:password) +UserController -> User : [!form.getUsername().equals(user.getUsername())] setPassword(String:password) UserController <-- User : boolean UserController -> UserManagement : save(User:LoggedIn) -UserManagement -> UserRepository : "save(User:LoggedIn)" +activate UserManagement +UserManagement -> UserRepository : save(User:LoggedIn) activate UserRepository UserManagement <-- UserRepository : User deactivate UserRepository UserController <-- UserManagement : User -activate UserManagement -UserController <-- UserManagement : User deactivate UserManagement -Spring <-- UserController : [!username.isBlank()] "redirect:/profile" -Spring <-- UserController : "redirect:/profile" deactivate Spring deactivate UserController deactivate User diff --git a/src/main/asciidoc/models/design/seq_users.svg b/src/main/asciidoc/models/design/seq_users.svg index c80d4c4..5806923 100644 --- a/src/main/asciidoc/models/design/seq_users.svg +++ b/src/main/asciidoc/models/design/seq_users.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e26f666700ba205672f26fddf3151f6ba044123f7ecc1f2ede119be3e3347cd8 -size 36951 +oid sha256:746deec5a361ba77933a3a9da52044251f286e966c9714e2995066f0fbcd7135 +size 37808 diff --git a/src/main/asciidoc/models/design/user.puml b/src/main/asciidoc/models/design/user.puml index d283c52..0cc1827 100644 --- a/src/main/asciidoc/models/design/user.puml +++ b/src/main/asciidoc/models/design/user.puml @@ -9,55 +9,68 @@ package Salespoint { class UserAccount class AbstractAggregateRoot class Role + class Errors } package Spring { class CrudRepository class Streamable + class WebDataBinder + class Errors + interface Validator } package catering.users { class User { - address : String - + User(userAccount : UserAccount, address : String) + - fullName : String + + User(userAccount : UserAccount, address : String, fullName : String) + User() + getAddress() : String + setAddress(address:String) : String + + getFullName() : String + + setFullName(fullName:String) : String + getUsername() : String + setUsername(username:String) : boolean + getUserAccount() : UserAccount + getId() : UserIdentifier + isEnabled() : boolean + hasRole(role:String) : boolean + + equals(obj:Object) : boolean + + hashCode() : int } - User ..> Role + User ..> WebDataBinder User --> UserAccount : "-userAccount" User --|> AbstractAggregateRoot : <> class UserController { + UserController(userManagement: UserManagement) + - initProfileBinder(binder : WebDataBinder) : void + - initRegistrationBinder(binder : WebDataBinder) : void + unauthorized() : String + register() : String - + register(form:RegistrationForm,result:Erros) : String + + register(form:RegistrationForm,result:Errors) : String + loginPage() : String + viewProfile(model:Model,userAccount:UserAccount) : String - + editProfile(userAccount:UserAccount,username:String,password:String,address:String) : String + + editProfile(userAccount:UserAccount,form:ProfileForm,result:Errors,model:Model) : String + disableUser(userAccount:UserAccount) : String + getCustomer(model:Model) : String - + removeCustomer(user:User,model:Model) : String + + removeCustomer(user:User) : String + editCustomer(user:User,model:Model) : String - + updateCustomer(user:User,username:String,address:String,model:Model) + + updateCustomer(user:User,username:String,address:String) } UserController --> UserManagement : "-userManagement" UserController ..> User UserController ..> UserAccount UserController ..> Role - UserController ..> RegistrationForm + UserController ..> UserForm + UserController ..> FormValidator + UserController ..> WebDataBinder class UserManagement { + UserManagement(users:UserRepository,userAccounts:UserAccountManagement) + createCustomer(name:String, password:String, address:String) : User - + createAdmin(name:String, password:String, address:String) : User + + createAdmin(name:String, password:String, address:String,fullName:String) : User + save(user:User) : User + getUsers() : UserRepository + disableUserAccount(userAccount:UserAccount) : void @@ -79,15 +92,22 @@ package catering.users { UserRepository --|> CrudRepository : <> UserRepository ..|> Streamable UserRepository o-- User - class RegistrationForm { + class UserForm { - username: String - password: String - address: String - + RegistrationForm(username:String,password:String,address:String) + - password: String + + UserForm(username:String,address:String,fullName:String,password:String) + getUsername() : String - + getPassword() : String + getAddress() : String - + validate(errors:Erros) : void + + getFullName() : String + + getPassword() : String } + class FormValidator { + supports(c:Class) : boolean + validate(o:Object,e:Errors) : void + } + FormValidator ..> Validator + FormValidator --> Errors } @enduml diff --git a/src/main/asciidoc/models/design/user.svg b/src/main/asciidoc/models/design/user.svg index 642f253..efcf11d 100644 --- a/src/main/asciidoc/models/design/user.svg +++ b/src/main/asciidoc/models/design/user.svg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63fdd760998f39a9a05a03dc275241c82e56737cff6a17c4e959991f9e8bf076 -size 40932 +oid sha256:18ed7c05540ac28246b9fda2572eb334808743451ba71d78e5657810930574c3 +size 51576 diff --git a/src/main/java/catering/users/FormValidator.java b/src/main/java/catering/users/FormValidator.java new file mode 100644 index 0000000..3566cbb --- /dev/null +++ b/src/main/java/catering/users/FormValidator.java @@ -0,0 +1,25 @@ +package catering.users; + +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +@Component +public class FormValidator implements Validator { + + @Override + public boolean supports(Class c) { + return UserForm.class.isAssignableFrom(c); + } + + @Override + @SuppressWarnings("unchecked") + public void validate(Object o, Errors e) { + F form = (F) o; + ValidationUtils.rejectIfEmptyOrWhitespace(e, "username", "username must not be empty"); + ValidationUtils.rejectIfEmptyOrWhitespace(e, "address", "address must not be empty"); + ValidationUtils.rejectIfEmptyOrWhitespace(e, "fullName", "fullName must not be empty"); + form.validatePassword(e); + } +} diff --git a/src/main/java/catering/users/ProfileForm.java b/src/main/java/catering/users/ProfileForm.java deleted file mode 100644 index 148c8c3..0000000 --- a/src/main/java/catering/users/ProfileForm.java +++ /dev/null @@ -1,38 +0,0 @@ -package catering.users; - -import jakarta.validation.constraints.NotEmpty; -import java.util.Optional; -import org.springframework.validation.Errors; - -public class ProfileForm { - - private final @NotEmpty String username, address, fullName; - private final Optional password; - - public ProfileForm(String username, Optional password, String address, String fullName) { - this.username = username; - this.address = address; - this.fullName = fullName; - this.password = password; - } - - public String getUsername() { - return username; - } - - public Optional getPassword() { - return password; - } - - public String getAddress() { - return address; - } - - public String getFullName() { - return fullName; - } - - public void validate(Errors errors) { - - } -} diff --git a/src/main/java/catering/users/RegistrationForm.java b/src/main/java/catering/users/RegistrationForm.java deleted file mode 100644 index 62f4cb9..0000000 --- a/src/main/java/catering/users/RegistrationForm.java +++ /dev/null @@ -1,36 +0,0 @@ -package catering.users; - -import jakarta.validation.constraints.NotEmpty; -import org.springframework.validation.Errors; - -public class RegistrationForm { - - private final @NotEmpty String password, username, address, fullName; - - public RegistrationForm(String username, String password, String address, String fullName) { - this.username = username; - this.address = address; - this.fullName = fullName; - this.password = password; - } - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - - public String getAddress() { - return address; - } - - public String getFullName() { - return fullName; - } - - public void validate(Errors errors) { - - } -} diff --git a/src/main/java/catering/users/UserController.java b/src/main/java/catering/users/UserController.java index 295817d..ddf276f 100644 --- a/src/main/java/catering/users/UserController.java +++ b/src/main/java/catering/users/UserController.java @@ -1,14 +1,17 @@ package catering.users; +import static catering.users.UserForm.RegistrationForm; +import static catering.users.UserForm.ProfileForm; import org.springframework.ui.Model; -import org.springframework.validation.Errors; - import org.salespointframework.useraccount.Role; import org.salespointframework.useraccount.UserAccount; import org.salespointframework.useraccount.web.LoggedIn; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -24,13 +27,26 @@ public class UserController { this.userManagement = userManagerment; }; + // the binder associates the form validator with the corresponding form + // as a result, it overrides every validator annotation + @InitBinder("profileForm") + private void initProfileBinder(WebDataBinder binder) { + binder.setValidator(new FormValidator()); + } + + @InitBinder("registrationForm") + private void initRegistrationBinder(WebDataBinder binder) { + binder.setValidator(new FormValidator()); + } + @GetMapping("/unauthorized") String unauthorized(){ return "unauthorized"; } @GetMapping("/register") - String register() { + String register(Model model) { + model.addAttribute("registrationForm", RegistrationForm.empty()); return "register"; } @@ -39,9 +55,6 @@ public class UserController { if (result.hasErrors()){ return "register"; } - if (form.getPassword().chars().anyMatch(Character::isISOControl)) { - return "register"; - } userManagement.createCustomer(form.getUsername(),form.getAddress(),form.getPassword(),form.getFullName()); return "redirect:/login"; } @@ -58,14 +71,18 @@ public class UserController { return "redirect:/"; } User user = userManagement.getUserByAccount(userAccount).get(); - model.addAttribute("user", user); + model.addAttribute("profileForm", new ProfileForm(user.getUsername(), user.getAddress(), user.getFullName(), "")); return "profile"; } @PostMapping("/profile") @PreAuthorize("isAuthenticated()") - public String editProfile(@LoggedIn UserAccount userAccount, @Valid ProfileForm form) { + public String editProfile(@LoggedIn UserAccount userAccount, @Valid ProfileForm form, Errors result, Model model) { String redirect = "redirect:/logout"; + + if (result.hasErrors()){ + return "profile"; + } User user = userManagement.getUserByAccount(userAccount).get(); if (form.getUsername().equals(user.getUsername())) { @@ -74,10 +91,8 @@ public class UserController { user.setUsername(form.getUsername()); user.setFullName(form.getFullName()); user.setAddress(form.getAddress()); - if (!form.getPassword().get().isEmpty()) { - if (form.getPassword().get().chars().anyMatch(Character::isISOControl)) { - userManagement.setPassword(form.getPassword().get(), user.getUserAccount()); - } + if (!form.getPassword().isEmpty()) { + userManagement.setPassword(form.getPassword(), user.getUserAccount()); } userManagement.save(user); diff --git a/src/main/java/catering/users/UserForm.java b/src/main/java/catering/users/UserForm.java new file mode 100644 index 0000000..98e0fa0 --- /dev/null +++ b/src/main/java/catering/users/UserForm.java @@ -0,0 +1,97 @@ +package catering.users; + +import org.springframework.validation.ValidationUtils; + +import java.util.Optional; + +import org.springframework.validation.Errors; + +/** + * An abstract class to hold form input for {@link User} data. + * + * It can be extended to validate input for different use cases. + */ +public abstract class UserForm { + + private final String username, address, fullName, password; + + public UserForm(String username, String address, String fullName, String password) { + this.username = username; + this.address = address; + this.fullName = fullName; + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getAddress() { + return address; + } + + public String getFullName() { + return fullName; + } + + public String getPassword() { + return password; + } + + /** + * Template Method for validating the password. + * + * @param e {@link Errors} from {@link FormValidator} + */ + public void validatePassword(Errors e) { + validatePasswordGeneric(e); + Optional.ofNullable(e.getFieldValue("password")).map(s -> (String) s).ifPresent(s -> { + if (s.chars().anyMatch(Character::isISOControl)) { + e.rejectValue("password", "password must only contain printable characters"); + } + }); + } + + /** + * Primitive Operation for validating the password depending on the subclass. + * + * @param e {@link Errors} from {@link FormValidator} + */ + public abstract void validatePasswordGeneric(Errors e); + + /** + * An extension of {@link UserForm} to validate input during registration. + * + * It requires the password to be not empty. + */ + public static class RegistrationForm extends UserForm { + RegistrationForm(String username, String address, String fullName, String password) { + super(username, address, fullName, password); + } + + public static RegistrationForm empty() { + return new UserForm.RegistrationForm("", "", "", ""); + } + + public void validatePasswordGeneric(Errors e) { + ValidationUtils.rejectIfEmptyOrWhitespace(e, "password", "password must not be empty"); + } + } + + /** + * An extension of {@link UserForm} to validate input during profile editing. + * + * The password can be empty. + */ + public static class ProfileForm extends UserForm { + ProfileForm(String username, String address, String fullName, String password) { + super(username, address, fullName, password); + } + + public void validatePasswordGeneric(Errors e) { + if (e.getFieldValue("password") == null) { + e.rejectValue("password", "password must not be null"); + } + } + } +} diff --git a/src/main/java/catering/users/UserManagement.java b/src/main/java/catering/users/UserManagement.java index 8e33254..e1ffdaf 100644 --- a/src/main/java/catering/users/UserManagement.java +++ b/src/main/java/catering/users/UserManagement.java @@ -56,4 +56,8 @@ public class UserManagement { public Optional getUserByAccount(UserAccount userAccount) { return users.findAll().stream().filter(u -> u.getUserAccount().equals(userAccount)).findFirst(); } + + public Optional getUserByName(String username) { + return userAccounts.findByUsername(username).flatMap(ua -> getUserByAccount(ua)); + } } diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html index ee7a6e8..e696a05 100644 --- a/src/main/resources/templates/profile.html +++ b/src/main/resources/templates/profile.html @@ -8,32 +8,34 @@