I am creating a new endpoint in this API, which accepts a request containing either a username or a 5 or 9-digit phone number. I need to add validations to validate the request to contain either a username OR phone number; if both are provided then return a validation error. Here is the current code I have.
#GetMapping("/users")
#Operation(summary = "API to return users based on username or phone number")
public ResponseEntity<List<UserResponseObject>> usersByNameOrNumber(
#Valid #RequestParam(name = "phone-number", required = false) #Pattern(regexp = "^(\\d{5}|\\d{9})$") String userPhoneNumber,
#Valid #PathVariable(name = "user-name", required = false) #Pattern(regexp = "^[a-zA-Z ]*$") String userName) {
try {
...
} catch (Exception e) {
...
}
}
How can I modify this block to meet the requirements? Currently, both fields are tagged as not required and there is no validation in place. I'd like it require ONLY one of these fields to be not null.
Try use optional, i dont know how solution you need.
#GetMapping("/users")
#Operation(summary = "API to return users based on username or phone number")
public ResponseEntity<List<UserResponseObject>> usersByNameOrNumber(
#Valid #RequestParam(value = "phoneNumber", required = false) #Pattern(regexp = "^(\\d{5}|\\d{9})$") Optional<String> phoneNumber,
#Valid #RequestParam(value = "userName", required = false) #Pattern(regexp = "^[a-zA-Z ]*$") Optional<String> userName) {
try {
if(phoneNumber.isPresent()){}
if(userName.isPresent()){}
} catch (Exception e) {
}
}
But i'm not sure the #Pattern and Optional will be work good, but probably yes.
Maybe is help, if i good understend your problem.
Related
I want to write a unit to allow users to change their personal Information. However, I don't know how to include the Security Context Holder mock into Unit Test. Especially, it is required to extract the user name which was used to find the User Information by query commands in User Repository. Thanks so much for your support.
Note:
I have successfully sent this edit Information API by using Postman before but it required you have login first and using Bearer JWT to edit user's information.
Below is my Unit Test:
#Test
public void whenSendRequestToModifyUserInformation_returnUserWithNewInformation () throws Exception {
String userName = "thanhnghi";
InformationRespondDTO informationRespondDTO = mock(InformationRespondDTO.class);
Information information = mock(Information.class);
ObjectMapper objectMapper = new ObjectMapper();
ModifyUserRequestDTO modifyUserRequestDTO =
ModifyUserRequestDTO.builder()
.dateOfBirth(new Date())
.firstName("Martin")
.lastName("Charlie")
.address("12 Washington District")
.phoneNumber("0794562342")
.email("martinCharlie#gmail.com").build();
;
when(informationService.update(modifyUserRequestDTO)).thenReturn(information);
when(informationMapper.toDTO(information)).thenReturn(informationRespondDTO);
mvc.perform(MockMvcRequestBuilders.put("/api/users/information")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(modifyUserRequestDTO))
)
.andExpect(status().isOk())
.andDo(print());
}
}
And this is my Service:
#Override
public Information update(ModifyUserRequestDTO modifyUserRequestDTO) {
String userName = userLocal.getLocalUserName();
Users users = this.userService.findByUserName(userName);
Information information = informationMapper.toExistedInformation(modifyUserRequestDTO, users.getInformation());
return this.informationRepository.save(information);
}
And this is my ModifyRequestDTO:
#Getter
#Setter
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class ModifyUserRequestDTO {
#NotNull(message = "date of birth is required")
private Date dateOfBirth;
#Pattern(regexp = "[A-Za-z]+", message = "First name cannot be number or special characters")
#NotNull(message = "First name cannot be null")
private String firstName;
#Pattern(regexp = "[A-Za-z]+", message = "Last name cannot be number or special characters")
#NotNull(message = "Last name is required")
#NotEmpty(message = "Last name must not be empty")
private String lastName;
#NotNull(message = "Address is required")
#NotEmpty(message = "Address must not be empty")
private String address;
#Size(min = 10, max = 11, message = "Phone number must has at least 11 characters and no more")
#NotNull(message = "phone number is required")
#NotEmpty(message = "phone number must not be empty")
private String phoneNumber;
// #Pattern(regexp = "[A-Za-z0-9]+#[a-zA-Z0-9]{6}", message = "Invalid Email Address")
#Email(message = "Invalid Email Address")
#NotNull(message = "email is required")
#NotEmpty(message = "email must not be empty")
private String email;
}
Component to handle Security Context Holder to look for userName:
#Component
public class UserLocal {
public String getLocalUserName(){
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
if(userName == null){
throw new ResourceNotFoundException("You haven't Login !!!");
}
return userName;
}
}
And finally UserController:
#RestController
#RequestMapping("/api/users")
#CrossOrigin(maxAge = 3600, origins = "*")
public class UserController {
UserService userService;
InformationService informationService;
InformationMapper informationMapper;
#Autowired
public UserController(UserService userService, InformationService informationService, InformationMapper informationMapper) {
this.userService = userService;
this.informationService = informationService;
this.informationMapper = informationMapper;
}
#PutMapping ("/information" )
public InformationRespondDTO modifyInformation(#RequestBody #Valid ModifyUserRequestDTO modifyUserRequestDTO){
Information information = this.informationService.update(modifyUserRequestDTO);
return informationMapper.toDTO(information);
}
}
And this is my error log:
java.lang.AssertionError: Status expected:<200> but was:<400>
Expected :200
Actual :400
Several ways to do it such as using UserRequestPostProcessor or #WithMockUser / #WithUserDetails or even a customised #WithXXXXUser. Refer to the docs for more details.
For example, using UserRequestPostProcessor as follows should solve your problem:
mvc.perform(MockMvcRequestBuilders
.put("/api/users/information")
.with(user("someUserName"))
I recently came up to an issue related to validation. Typically, I am building a REST api that allow users to create their account including avatars. All of the information should be submitted when user clicks to Register button. So, my server will then receive a request that includes some fields like name (string), birthday (datetime), ... and avatar (multipart file). So, the question is how to validate the received file is a truly image and has an allowed size and simultaneously validate that the others (email, password) are also valid.
For the case that all fields is text, we can easily validate them using the combination of annotations like this
Controller
#PostMapping(path = "")
public ResponseEntity<?> createNewAccount(#RequestBody #Valid RegisterRequest registerRequest) {
Long resourceId = service.createNewCoderAccount(registerRequest);
return ResponseEntity.created(location(resourceId)).build();
}
Request DTO
#ConfirmedPassword
public class RegisterRequest extends BaseRequest implements ShouldConfirmPassword {
#NotBlank(message = "Field 'email' is required but not be given")
#Email
#Unique(message = "Email has been already in use", service = UserValidatorService.class, column = "email")
private String email;
#NotBlank(message = "Field 'password' is required but not be given")
#Size(min = 6, message = "Password should contain at least 6 characters")
private String password;
#NotBlank(message = "Field 'confirmPassword' is required but not be given")
private String confirmPassword;
#NotBlank(message = "Field 'firstName' is required but not be given")
private String firstName;
#NotBlank(message = "Field 'lastName' is required but not be given")
private String lastName;
}
Or in case that the request containing only file(s), we can absolutely do like this
Controller
#PostMapping(path = "/{id}")
public ResponseEntity<?> editChallengeMetadata(
#ModelAttribute ChallengeMetadataRequest request,
BindingResult bindingResult,
#PathVariable("id") Long id,
#CurrentUser User user
) throws BindException {
challengeMetadataRequestValidator.validate(request, bindingResult);
if (bindingResult.hasErrors()) {
throw new BindException(bindingResult);
}
Long challengeId = service.updateChallengeMetadata(id, request, user);
return ResponseEntity.ok(RestResponse.build(challengeId, HttpStatus.OK));
}
Validator
public class ChallengeMetadataRequestValidator implements Validator {
#Override
public boolean supports(#NonNull Class<?> aClass) {
return ChallengeMetadataRequest.class.isAssignableFrom(aClass);
}
#Override
public void validate(#NonNull Object o, #NonNull Errors errors) {
ChallengeMetadataRequest request = (ChallengeMetadataRequest) o;
if (request.getBanner() != null && !request.getBanner().isEmpty()) {
if (!List.of("image/jpeg", "image/png").contains(request.getBanner().getContentType())) {
errors.rejectValue("banner", "challenge.mime-type.not-supported", new String[]{request.getBanner().getContentType()}, "Mime-type is not supported");
}
}
}
}
As you seen above, if I wrap all data (including avatar) in a DTO class, I definitely write its own validator. But what will happen if then I have to write manually hundreds validators like that.
So, do anyone have any idea about it, typically, make the multipart/form-data request becomes simalar with application/json request ?
Thanks and regards,
I've stumbled upon interesting case and I'm not sure how to resolve it. It's probably related to JSON Post request for boolean field sends false by default but advices from that article didn't help.
Let's say I have this class:
public class ReqBody {
#NotNull
#Pattern(regexp = "^[0-9]{10}$")
private String phone;
//other fields
#NotNull
#JsonProperty(value = "create_anonymous_account")
private Boolean createAnonymousAccount = null;
//constructors, getters and setters
public Boolean getCreateAnonymousAccount() {
return createAnonymousAccount;
}
public void setCreateAnonymousAccount(Boolean createAnonymousAccount) {
this.createAnonymousAccount = createAnonymousAccount;
}
}
I also have endpoint:
#PostMapping(value = "/test", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MyOutput> test(
#ApiParam(value = "information", required = true) #RequestBody ReqBody input
) {
//do something
}
problem is when I send my request body as:
{
"phone": "0000000006",
"create_anonymous_account": null
}
or just like
{
"phone": "0000000006"
}
it sets createAnonymousAccount to false.
I have checked, and it correctly recognises "create_anonymous_account": true
Is there any way to "force" null value in boolean field?
I really need to know if it was sent or no, and not to have default value.
You can use Jackson annotation to ignore the null fields. If the Caller doesn't send createAnonymousAccount then it will be null.
#JsonInclude(JsonInclude.Include.NON_NULL)
public class ReqBody {
#NotNull
#Pattern(regexp = "^[0-9]{10}$")
private String phone;
//other fields
#JsonProperty(value = "create_anonymous_account")
private Boolean createAnonymousAccount ;
}
I use Spring 4 and Hibernate 5
I have User class with password field with custom validator.
I need to validate it while form binding to be 8 characters long and include lowercase and uppercase letters, and numbers.
When user enters password it is valid, but it is not valid when i encode it.
So is there a way to make my custom validation annotation to be ignored on persist?
I know I can make different field for unencrypted password, or make data transfer object, validate it and then pasrse it's data to User.
But I am interested in possibility of annotation parametrization.
#Entity
#Table(name = "user")
public class User {
//other fields
#NotNull
#NotEmpty
#ValidPassword
#Column(name = "password", nullable = false, length = 60)
private String password;
//getters and setters
}
My validator
#Target({ TYPE, FIELD, ANNOTATION_TYPE })
#Retention(RUNTIME)
#Constraint(validatedBy = PasswordValidator.class)
#Documented
public #interface ValidPassword {
String message() default "Password is too short! Must be 8 digits and include lowercase, uppercase letters and numbers.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
private Pattern pattern;
private Matcher matcher;
private static final String PATTERN = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,})";
#Override
public void initialize(ValidPassword constraintAnnotation) {
}
#Override
public boolean isValid(String password, ConstraintValidatorContext context) {
return (validate(password));
}
private boolean validate(String password) {
pattern = Pattern.compile(PATTERN);
matcher = pattern.matcher(password);
return matcher.matches();
}
}
Controller method
#RequestMapping(value = "/registeruser", method = RequestMethod.POST)
public String registerUser(#ModelAttribute("user") #Valid User user, BindingResult result, Model model) {
if (result.hasErrors()) {
model.addAttribute("errorSummary", result.getFieldErrors().stream()
.map(e -> e.getField() + " error - " + e.getDefaultMessage() + " ").collect(Collectors.toList()));
model.addAttribute("user", user);
} else {
User registered = null;
registered = createUserAccount(user, result);
if (registered == null) {
model.addAttribute("errorSummary", "User with this email already registered!");
model.addAttribute("user", user);
return "registration";
}
model.addAttribute("flashMessage", "User registered successfully!");
}
return "registration";
}
UserService implementation method(where I encode my password)
#Transactional
#Override
public User registerNewUserAccount(User user) throws EmailExistsException {
if (emailExist(user.getEmail())) {
throw new EmailExistsException("There is an account with that email address:" + user.getEmail());
}
if (user.getPassword() == null) {
user.setPassword(new BigInteger(130, new SecureRandom()).toString(32));
System.out.println("+++++++++++++++" + user.getPassword());
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setUserRole(new HashSet<UserRole>(1));
user.getUserRole().add(new UserRole(user, Constants.RoleType.USER.name()));
save(user);
return user;
}
By default validation will happen for all constraints. Or you can specify Grouping Constraints
You can create a group by creating an interface:
interface FormValidationGroup{}
And annotate the password field like this:
#ValidPassword(groups = FormValidationGroup.class)
private String password;
Documentation for Custom Constraints annotations where groups parameter is mentioned.
Hibernate Validator should now ignore the password field unless you specify the group for validation. In order to specify a group for validation of a parameter of a Spring MVC handler method, use the Validated annotation instead of Valid. E.g.:
String registerUser(#ModelAttribute #Validated(FormValidationGroup.class) User user,
BindingResult result, Model model) {
if (result.hasErrors()) {
I'm building an Spring Boot application which allows registering, submitting of various data and etc which requires validation. For example I have this entity setup with basic constraints:
#Entity
public class Account extends BaseEntity {
public static final int MAX_LENGTH = 64;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String username;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String email;
...
}
Each time before creating a new account, validation is performed on the service layer:
public Account register(String username, String email, String rawPassword) {
if (!accountValidator.validate(username, email, rawPassword)) {
return null;
}
Account account = new Account(username, email, passwordEncoder.encode(rawPassword));
try {
return accountRepository.save(account);
} catch (DataIntegrityViolationException e) {
return null;
}
}
The validator snippet:
public boolean validate(String username, String email, String password) {
if (!validateUsernameStructure(username)) {
return false;
}
if (!validateEmailStructure(email)) {
return false;
}
if (!validatePasswordStructure(password)) {
return false;
}
// Performs a query on all users to see if no such email or username
// already exists. Before checking the email and username are set to
// lowercase characters on the service layer.
if (accountService.doesEmailOrUsernameExist(email, username)) {
return false;
}
return true;
}
Now in this case if I get a lot of multiple requests and one of them manages to get past the validation, I will encounter an exception if the username or email is forced to be lowercase in the first place. But for example I want to allow users to register with upper case/lower case username, email characters and etc. Based on this question I could add a additional field to my entity or add a more complex constraint on the database level, however I want to do this without overflow data and in java code.
So for example I get these two requests to create a new account milliseconds apart:
Request 1:
username=foo
email=foo#foo.com
Request 2:
username=foO
email=foO#foO.com
During this phase I check for duplicates (email and username is set to lower case, however when saving I keep the case intact):
if (accountService.doesEmailOrUsernameExist(email, username)) {
return false;
}
However since the requests are so close to each other, the first request might not have created a new account yet so the check for the second account passes and thus I get two database entries.
So the actual question would be, how to perform thread safe validation for these kinds of actions in my service layer without huge performance impact?
Solution I've chosen for this example
When setting the username and email, also apply those values to their lowercase counterparts and apply unique constraints on them as well. This way I get to keep the user set case for those two properties:
#Entity
public class Account extends BaseEntity {
public static final int MAX_LENGTH = 64;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private final String username;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String email;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private final String normalizedUsername;
#Column(unique = true, nullable = false, length = MAX_LENGTH)
private String normalizedEmail;
public Account(String username, String email) {
this.username = username;
this.email = email;
this.normalizedUsername = username.toLowerCase();
this.normalizedEmail = email.toLowerCase();
}
...
}
There is no simple solution without the help of database because transactions are isolated from each other.
The alternative would be to temporarily store usernames/emails in memory and do complex synchronizations to perform the checks. Things would be even more complicated in clustered environments. This is very ugly and hard to maintain (and may impact performance significantly if the lock for single synchronization point is held for too long).
The standard and straightforward solution is to use database constraints.