I've encountered this problem multiple times.
The problem:
I have some class with private Fields (For example User information which is a Dto):
public class RegisterRequest {
private String firstName;
private String lastName;
private String username;
private String email;
private String fieldOfStudy;
private String password;
}
After searching the community on how to read the values of these fields (when for example doing a post request), I saw a lot of answers that said the solution is Reflection.
Lets say I want to check if any field is null (in another class), then my reflection-method would be the following:
for (Field f : registerRequest.getClass().getDeclaredFields()) {
try {
Field field = registerRequest.getClass().getDeclaredField(f.getName());
field.setAccessible(true);
Object value = field.get(registerRequest);
if (value == null) {
throw new AccountException("Vul alle velden in.");
}
}
catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
Now I'm wondering if there is a way to do this without using field.setAccesible(true), Since bypassing the accessibility of fields could lead to run-time errors. I.e. We shouldn't use reflection to change the visibility of a field.
Thank you for your time!
Do the validity check in the class that contains the private Data:
public boolean isValidForm() {
for (Field field : this.getClass().getDeclaredFields()) {
try {
if (field.get(this) == null) {
return false;
}
} catch (IllegalAccessException e) {
throw new AccountException("Something went wrong");
}
}
return true;
}
Use this function in your other class by doing:
if(!registerRequest.isValidForm) {
throw new AccountException("...")
}
Credits to Geocodezip
Maybe there's something I didn't undertsand in your post but I don't think you should use reflection to do validation on your DTO. Here's a solution on how you could handle validation using javax:
#Data
#Value
class RegisterRequest {
#NotNull(message = "First name should not be null")
String firstName;
#NotNull(message = "Last name should not be null")
String lastName;
#NotNull(message = "Username should not be null")
String username;
#NotNull(message = "Email should not be null")
String email;
#NotNull(message = "Field of study should not be null")
String fieldOfStudy;
#NotNull(message = "Password should not be null")
String password;
}
If you use java17+ you can use a record like this (and get rid of lombok):
public record RegisterRequest(
#NotNull(message = "First name should not be null")
String firstName,
#NotNull(message = "Last name should not be null")
String lastName,
#NotNull(message = "Username should not be null")
String username,
#NotNull(message = "Email should not be null")
String email,
#NotNull(message = "Field of study should not be null")
String fieldOfStudy,
#NotNull(message = "Password should not be null")
String password
) {
}
Then you can make a unit test on your validation like this:
class RegisterRequestTest {
private Validator validator;
#BeforeEach
public void setUp() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
validator = factory.getValidator();
}
}
#ParameterizedTest
#MethodSource("invalidRegisterRequest")
void validation_should_be_invalid(RegisterRequest registerRequest) {
assertThat(validator.validate(registerRequest)).isNotEmpty();
}
private static List<RegisterRequest> invalidRegisterRequest() {
return List.of(
new RegisterRequest(null, "lasstname", "username", "email", "fieldOfStudy", "password"),
new RegisterRequest("firstName", null, "username", "email", "fieldOfStudy", "password"),
new RegisterRequest("firstName", "lasstname", null, "email", "fieldOfStudy", "password"),
new RegisterRequest("firstName", "lasstname", "username", null, "fieldOfStudy", "password"),
new RegisterRequest("firstName", "lasstname", "username", "email", null, "password"),
new RegisterRequest("firstName", "lasstname", "username", "email", "fieldOfStudy", null)
);
}
}
In my examples I used #NotNull like in your post but I would recommand to use #NotBlank because in addition to checking for nullity, it checks if the property contains at least one non-blank character.
Related
JDK 17
SpringBoot latest
JPA latest
MySQL 8.0.31
I am trying to implement a strategy that makes sure that both the name and the email address of each user are unique.
User entity:
#Entity
public class User {
......
#EmbeddedId
protected UserId id;
......
}
User id:
#Embeddable
public class UserId implements Serializable {
#Serial
private static final long serialVersionUID = -622156255674132106L;
#Column(name = "name", nullable = false)
protected String name = "";
#Column(name = "email", nullable = false)
protected String email = "";
public UserId(String name, String email) {
setName(name);
setEmail(email);
}
public UserId() {}
public void setName(String name) {
this.name = name;
}
public String getName() {
return Objects.requireNonNullElse(name, "");
}
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return Objects.requireNonNullElse(email, "");
}
}
Now, by default, it is marked as a conflict only if userA.name == userB.name && userA.email == userB.email, which means there can be two users having the same email address as long as they do not share one single name. How to stop this from happening? What I expect is userA.name == userB.name || userA.email == userB.email.
I've tried overriding equals() and hashcode() in the following way.
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserId userId)) return false;
if (Objects.equals(name, userId.name)) return true;
return Objects.equals(email, userId.email);
}
#Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (email != null ? email.hashCode() : 0);
return result;
}
However, it does not work. Also, breakpoints inside these two functions are not reached.
==========Edited==========
I've solved the original problem. But when it comes to UsersRepository.existsById(), it's still considered not to exist if either of the two columns does not match. How can I fix it?
Whether you do this via annotations and schema generation, or just by creating / modifying the schema directly, the answer is the same.
You will need to create a single unique constraint in the database naming both columns, not two separate constraints.
If you want a schema generation annotation to do this, supply the #UniqueConstraint annotation to the #Table annotation, e.g.
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = {
"name", "email"
})
})
public class UserId implements Serializable {
#Serial
private static final long serialVersionUID = -622156255674132106L;
#Column(name = "name", nullable = false, unique=true)
protected String name = "";
#Column(name = "email", nullable = false, unique=true)
protected String email = "";
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'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 am making a restaurant management app and I am having a problem persisting a user association. For the Chef class there is a set association with the Dish class so that specific dishes can be associated with a certain chef.
I have created a method that associates a dish with a chef, and when I try calling it on my REST client the method seems to work, and it returns a JSON of the chef object with the updated info, however when I call the get chef method the JSON no longer shows the added dish item
Here is the chef class and everything related to the Dish Object
#Table(name="chef")
public class Chef {
//Chef Attributes
#Id private String username;
private String firstName;
private String lastName;
private String email;
private String password;
private String address;
private String bio;
private Boolean delivery;
private String photoURL;
// #OneToOne
// private ChefMenu menu;
#Transient
#OneToMany(mappedBy = "chef", cascade = CascadeType.ALL)
#JsonIgnoreProperties("chef")
private Set<Dish> menuItems;
public Set<Dish> getMenuItems() {
if (this.menuItems == null) {
this.menuItems = new HashSet<Dish>();
}
return this.menuItems;
}
Here is the Dish class with everything related to the Chef class
#Entity
#Table(name="dish")
public class Dish {
//Dish Attributes
#Id private String dishName;
private String cuisine;
private double price;
private String maxQuantity;
private String dietaryRestriction;
private String mealIngredients;
private String cookingTime;
#ManyToOne
#JoinColumn(name = "chef")
#JsonIgnoreProperties({"menuItems","orders","firstName", "lastName", "email", "bio", "password", "address", "delivery", "photoURL"})
private Chef chef;
public void setChef(Chef val) { this.chef = val; }
public Chef getChef() {
return this.chef;
}
Here is the method used to add a new dish to a chef from the repository
#Transactional
public Chef addDishToMenu(Chef c, Dish d) {
c.addDish(d);
entityManager.merge(c);
return c;
}
And finally here is code from the controller class:
#PostMapping("dish/create/{dishName}/{cuisine}/{price}/{maxQuantity}/{dietaryRestriction}/{mealIngredients}/{cookingTime}")
public ResponseEntity createDish(String username,
#PathVariable("dishName") String dishName, #PathVariable("cuisine") String cuisine,
#PathVariable("price") String price, #PathVariable("maxQuantity") String maxQuantity,
#PathVariable("dietaryRestriction") String dietaryRestriction,
#PathVariable("mealIngredients") String mealIngredients, #PathVariable("cookingTime") String cookingTime)
{
Dish d = new Dish();
Double p = Double.parseDouble(price);
//int mQ = Integer.parseInt(maxQuantity);
try {
d = foodRepository.createDish(dishName, cuisine, p, maxQuantity, dietaryRestriction,
mealIngredients, cookingTime);
} catch (InvalidInputException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new Response(false, e.getMessage()));
}
return ResponseEntity.status(HttpStatus.OK).body(d);
}
#PostMapping("dish/add/{username}/{dishName}")
public ResponseEntity addDishToMenu(#PathVariable("username") String username, #PathVariable("dishName") String dishName) throws NullObjectException {
Chef c = new Chef();
Dish d = new Dish();
c= chefRepository.getChef(username);
d = foodRepository.getSpecificDish(dishName);
c = foodRepository.addDishToMenu(c, d);
return ResponseEntity.status(HttpStatus.OK).body(c);
}
#GetMapping("/get/{username}")
public ResponseEntity getChef(#PathVariable("username") String username) {
// List<AppUser> user;
Chef chef = new Chef();
try {
chef = chefRepository.getChef(username);
// user = userRepository.getAppUserQuery(username);
} catch (NullObjectException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new Response(false, e.getMessage()));
}
return ResponseEntity.status(HttpStatus.OK).body(chef);// user.get(0));
}
So when I make the call on my Rest client to add a dish to a chef I get this as a response:
{
"username": "Favreau4",
"firstName": "Jon",
"lastName": "Favreau",
"email": "chefFavreau#email.com",
"password": "j+9UECq/PLA=$I+HsXngf/b82+rMtPQQO",
"address": null,
"bio": null,
"delivery": null,
"photoURL": null,
"menuItems": [
{
"dishName": "Big sandwich",
"cuisine": "american",
"price": 5,
"maxQuantity": "3",
"dietaryRestriction": "bacon",
"mealIngredients": "bacon,lettuce,tomato,bread",
"cookingTime": "10mins"
}
],
"order": [],
}
but when I use the getChef REST call I get this:
{
"username": "Favreau4",
"firstName": "Jon",
"lastName": "Favreau",
"email": "chefFavreau#email.com",
"password": "j+9UECq/PLA=$I+HsXngf/b82+rMtPQQO",
"address": null,
"bio": null,
"delivery": null,
"photoURL": null,
"menuItems": [],
"order": [],
}
Does anyone know how to resolve this issue?
Are you aware of #Transient annotation? Transient are used to mark a variable as non-presistable. So your menuitems are not getting persist or saved in database.
I have a dto like this
#FieldMatch(first = "email", second = "emailConfirm", message = "E-posta adresleri eslesmeli")
public class EmailDTO {
private String email;
private String emailConfirm;
this is validator
public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
private String firstFieldName;
private String secondFieldName;
private String message;
#Override
public void initialize(final FieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
message = constraintAnnotation.message();
}
#Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
boolean valid = true;
try
{
final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
valid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
}
catch (final Exception ignore)
{
// ignore
}
if (!valid){
context.buildConstraintViolationWithTemplate(message)
.addPropertyNode(firstFieldName)
.addConstraintViolation()
.disableDefaultConstraintViolation();
}
return valid;
}
}
this is interface
arget({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = FieldMatchValidator.class)
#Documented
public #interface FieldMatch {
String message() default "The fields must match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first();
String second();
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Documented
#interface List
{
FieldMatch[] value();
}
}
but i also want to change the class to this
#FieldMatch(first = "email", second = "emailConfirm", message = "E-posta adresleri eslesmeli")
public class EmailDTO {
#Max(value = 150, message = "E-posta karakter sayisi fazla!")
#Email(message = "Email should be valid")
#NotNull(message = "Name cannot be null")
#NotEmpty(message = "Name cannot be null")
private String email;
#Max(value = 150, message = "E-posta karakter sayisi fazla!")
#Email(message = "Email should be valid")
#NotNull(message = "Name cannot be null")
#NotEmpty(message = "Name cannot be null")
private String emailConfirm;
Should I use another generic constraint or a lot of annotations like shown above? I also have other entities like password, etc and those entities will also have same validations.
Some fields can be nullable, so not every field has to be checked by null constraint. I want to do a very generic validation. I will send to front end (thymeleaf), so I need to see which constraint is violated.
Email has #email annotation that wont be in the password validation. Others are common. like
notnull, notempty, matching, notblank
I was not able to find good examples. I found below posts on the topic, but I could not find an example of custom + for example #email validation together.
spring-custom-annotation-validation-with-multiple-field
how-can-i-validate-two-or-more-fields-in-combination
spring-mvc-custom-validator post shows validation for different fields.