So. I'm trying to have a custom validator for my method. This one:
#PostMapping("/person")
public Person create(#Validated(value = IPersonValidator.class) #RequestBody Person person, Errors errors) {
LOGGER.info("Creating a person with the following fields: ");
LOGGER.info(person.toString());
if (errors.hasErrors()) {
LOGGER.info("ERROR. Error creating the person!!");
return null;
}
//return personService.create(person);
return null;
}
This is my Validator class:
#Component
public class PersonValidator implements Validator, IPersonValidator {
#Override
public boolean supports(Class<?> clazz) {
return Person.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Person person = (Person) target;
if (person.getName() == null || person.getName().trim().isEmpty()) {
errors.reject("name.empty", "null or empty");
}
}
}
And my interface:
public interface IPersonValidator {
}
And my class:
#Entity
#Table(name = "persons")
#Data
#Getter
#Setter
#NoArgsConstructor
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "person_sequence")
#SequenceGenerator(name = "person_sequence", sequenceName = "person_sequence", allocationSize = 1)
#Column(name = "id")
private Long id;
//#NotBlank(message = "Name cannot be null or empty")
#Column(name = "name")
private String name;
#NotBlank(message = "Lastname cannot be null or empty")
#Column(name = "last_name")
private String lastName;
#Column(name = "last_name_2")
private String lastName2;
public Person(String name, String lastName, String lastName2) {
this.name = name;
this.lastName = lastName;
this.lastName2 = lastName2;
}
}
What am I expecting is for it to enter the validate method since Im using the annotation (#Validate) with the class that I want. I tried it too using the #Valid annotation, but it still won't enter in my custom validate method.
What am I doing wrong?
This code #Validated(value = IPersonValidator.class) is use for "grouping" of validations. Yoy say to validator valid only these constrains with groups = {IPersonValidator.class}.
For better understanding of custom validators see: https://www.baeldung.com/spring-mvc-custom-validator
edit:
This code #Validated(value = IPersonValidator.class) doesn't use your validator. From #Validated documantation:
value() specify one or more validation groups to apply to the
validation step kicked off by this annotation.
Validation groups are use for turning on/off of some valdations for some fields in data class. Se more here: https://www.baeldung.com/javax-validation-groups
For creating you own custom valdation you must create custom constrain annotation and custom validator for this annotation. This custom constrain annotation you may use on some field in data class. I really strong recommend read this https://www.baeldung.com/spring-mvc-custom-validator
Related
I have Spring Boot application (v3.0.2, Java 17), and in it, a simple entity ActivityType and corresponding ActivityDto.
//Entity (uses Lombok 1.18.24)...
#Getter
#Setter
#Entity
public class ActivityType {
#Id
#Column(name = "ActivityTypeId", nullable = false)
private Integer id;
#Column(name = "ActivityName", nullable = false, length = 30)
private String activityName;
#Column(name = "ActivityDescription")
private String activityDescription;
}
//DTO...
public record ActivityTypeDto(
Integer id,
String activityName,
String activityDescription) implements Serializable {
}
I'm using IntelliJ Idea (v2022.2.4) and JPA Buddy (v2022.5.4-222) to generate the Mapper Interface (MapStruct v1.5.3.Final). When I build the Mapper implementation, in the generated code, both the toEntity and toDto methods are incorrect.
#Component public class ActivityTypeMapperImpl implements ActivityTypeMapper {
#Override
public ActivityType toEntity(ActivityTypeDto activityTypeDto) {
if ( activityTypeDto == null ) {
return null;
}
ActivityType activityType = new ActivityType();
return activityType;
}
#Override
public ActivityTypeDto toDto(ActivityType activityType) {
if ( activityType == null ) {
return null;
}
// What's this all about?? Why not activityType.id, etc??
Integer id = null;
String activityName = null;
String activityDescription = null;
ActivityTypeDto activityTypeDto = new ActivityTypeDto( id, activityName, activityDescription );
return activityTypeDto;
}
#Override
public ActivityType partialUpdate(ActivityTypeDto activityTypeDto, ActivityType activityType) {
if ( activityTypeDto == null ) {
return activityType;
}
return activityType;
}
I've tried various alternatives, including using a class for the DTO instead of a record, but no success. Looks like I've missed something, but not sure what.
Update:
I can fix this by not using Lombok for the Entity getters/setters, which leads me on to final question, is there a setting on the MapStruct plugin to take Lomboz into account?
please define you entity like this,
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ActivityType {
#Id
#Column(name = "ActivityTypeId", nullable = false)
private Integer id;
#Column(name = "ActivityName", nullable = false, length = 30)
private String activityName;
#Column(name = "ActivityDescription")
private String activityDescription;
}
then define ActivityTypeDTO like this,
#Data
public class ActivityTypeDTO {
#JsonProperty("id")
private Integer id;
#JsonProperty("ActivityName")
private String ActivityName;
#JsonProperty("activityDescription")
private String activityDescription;
best practice to use MapStruct is like this,
#Mapper(componentModel = "spring", uses = {})
public interface ActivityMapper extends EntityMapper<ActivityTypeDTO, ActivityType> {
ActivityTypeDTO toDto(ActivityType activityType);
ActivityType toEntity(ActivityTypeDTO activityTypeDTO);
}
and EntityMApper in Mapper should be like this,
public interface EntityMapper<D, E> {
E toEntity(D dto);
D toDto(E entity);
}
Now I am sure you mapper work correctly.
I want to create a unit test that will use reflection to find all missing fields in dto that implement BaseDto by their persistence entities. This is what I did.
#Slf4j
public class EntityAuditDtoTest {
#Test
public void find_MissingAndExtraFieldsThatUsedInAuditDtosByEntity_ReturnMissingAndExtraFields() throws ClassNotFoundException {
// Arrange
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(AuditEntityType.class));
// Find all classes annotated with #AuditEntityType in the package com.example.dto
Set<BeanDefinition> auditDtoBeans = scanner.findCandidateComponents("com.example.dto");
// Act
for (BeanDefinition auditDtoBean : auditDtoBeans) {
Class<?> auditDtoClass = Class.forName(auditDtoBean.getBeanClassName());
// Make sure the DTO class implements BaseAuditDto
if (!BaseAuditDto.class.isAssignableFrom(auditDtoClass)) {
continue;
}
Class<?> entityClass = getEntityClassForDto(auditDtoClass);
Field[] dtoFields = auditDtoClass.getDeclaredFields();
Field[] entityFields = entityClass.getDeclaredFields();
List<String> missingFields = Arrays.stream(entityFields).map(Field::getName)
.filter(field -> Arrays.stream(dtoFields).noneMatch(f -> f.getName().equals(field))).toList();
if (!missingFields.isEmpty()) {
log.error("Missing fields in DTO class: {} \nfor entity class: {} : {}", auditDtoClass.getName(),
entityClass.getName(), missingFields);
}
List<String> extraFields = Arrays.stream(dtoFields).map(Field::getName)
.filter(field -> Arrays.stream(entityFields).noneMatch(f -> f.getName().equals(field))).toList();
if (!extraFields.isEmpty()) {
log.error("Extra fields in DTO class: {} \nfor entity class: {} : {}", auditDtoClass.getName(),
entityClass.getName(), extraFields);
}
}
}
}
But the problem is that the dto may have a field that is in the entity class, but the test will think that this is a missing field.
For example:
Dto class: ContractAudit has customerId field (customerId). And ContractEntity has public CustomerEntity customer.
This is the same fields. But of course for test they are different. I don't understand how to ignore them. I also don't want to hardcode filter that skip all endings with 'id' prefix.
#Data
#AuditEntityType("Contract")
public class ContractAudit implements BaseAuditDto {
private Long id;
private String ref;
private String status;
private Long customerId;
}
#Entity
#Table(name = "contract")
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class ContractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
#ToString.Include
private Long id;
#Column(name = "ref", updatable = true)
#ToString.Include
private String ref;
#Column(name = "status")
#ToString.Include
#Enumerated(value = EnumType.STRING)
private ContractStatusEnum status;
#ManyToOne
#JoinColumn(name = "customer_id")
public CustomerEntity customer;
#Column(name = "deleted")
#ToString.Include
private boolean deleted;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "contract_id")
private List<ContractDocumentEntity> documents;
}
Output:
Missing fields in DTO class: ContractAudit for entity class: ContractEntity : [customer, deleted, documents]
Extra fields in DTO class: ContractAudit for entity class: ContractEntity : [customerId]
I want to have missing fields: [deleted, documents]
If you have any other ideas on how to do this, I'd love to hear it. I am not asking for implementation. Suggestions only)
Lol. I found solution for my case.
My previous approach was incorrect. Because it's impossible to find 'missing' and 'extra' fields by name correctly for every case. I decided to use:
assertThat(entityClass.getDeclaredFields()).hasSameSizeAs(auditDtoClass.getDeclaredFields());
So this code is checking if the entityClass and the DtoClass have the same number of fields (properties) declared. If not it fail test and print all fields from each classes. If anyone has better ideas I'll be happy to hear.
Postman error
Controller:
#RestController
#RequestMapping("/student_enrollment/class_subject")
public class ClassController {
private ClassService classService;
public ClassController(ClassService classService) {
super();
this.classService = classService;
}
#PostMapping()
public ResponseEntity<Classes> saveClass(#RequestBody Classes classes) {
return new ResponseEntity<Classes>(classService.saveClass(classes),
HttpStatus.CREATED);
}
#GetMapping
public List<Classes> getAllClasses() {
return classService.getAllClasses();
}
model:
#Data
#Entity
#Table(name = "class_subject")
public class ClassSubject {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int subject_id;
#Column(value = "prerequisite")
private String prerequisite;
#Column(value = "max_capacity")
private int max_capacity;
}
service impl:
#Service
public class ClassServiceImpl implements ClassService{
#Override
public ClassSubject updateClassSubject(ClassSubject classSubject, int classSubjectId) {
ClassSubject existingClassSubject = classSubjectRepository.findById(classSubjectId).orElseThrow(() -> new ResourceNotFoundException("ClassSubject", "classSubjectId", classSubjectId));
existingClassSubject.setSubject_id(classSubject.getSubject_id());
existingClassSubject.setPrerequisite(classSubject.getPrerequisite());
existingClassSubject.setMax_capacity(classSubject.getMax_capacity());
classSubjectRepository.save(existingClassSubject);
return existingClassSubject;
}
Service:
public interface ClassService {
Classes saveClass(Classes classes);
List<Classes> getAllClasses();
Classes getClassByID(int classId);
Classes updateClass(Classes classes, int classId);
void deleteClass(int classId);
}
SQL Queries:
CREATE TABLE class_subject(
subject_id INT NOT NULL,
prerequisite VARCHAR(30),
max_capacity INT NOT NULL,
PRIMARY KEY (subject_id)
);
Because in Postman you are using PUT method, not the POST method.
Let me guess you are expecting 'subject_id' to be auto-generate using database sequence. Then you are missing two annotations #GeneratedValue and #SequenceGenerator.
As 'subject_id' is not an optional field. That's why you getting the error "Field 'subject_id' doesn't have a default value".
To create a relationship between the 'subject_id' field and DB sequence use the following.
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#SequenceGenerator(name="seq",sequenceName="sequence_in_db")
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq")
private int subject_id;
You can find more details on this topic what is the use of annotations #Id and #GeneratedValue(strategy = GenerationType.IDENTITY)? Why the generationtype is identity?
I have service which is getting values from api and mapping it by model mapper to the unified entity.
The problem is that UUID is not working. I am getting null instead of any string id (I had to change name of if for "uniqueIdentifier" becouse objects from api had "id" field).
My entity:
#Entity
#Getter
#Setter
#Table(name = "unifiedOffers")
#AllArgsConstructor
#NoArgsConstructor
public class UnifiedOfferEntity {
#Id
#GeneratedValue(generator = "system-uuid")
#GenericGenerator(name = "system-uuid", strategy = "uuid")
#Column(name = "id", updatable = false, nullable = false)
private String uniqueIdentifier;
private String companyName;
private String city;
private String street;
private String title;
private LocalDateTime posted;
private String url;
}
Endpoint for tests:
#GetMapping("/test")
public String getOffers() throws ExecutionException, InterruptedException {
List<UnifiedOfferEntity> result = jobFinderService.getAllOffers();
for (UnifiedOfferEntity unifiedOfferEntity : result) {
System.out.println(unifiedOfferEntity.getUniqueIdentifier() + " " + unifiedOfferEntity.getTitle() + " " + unifiedOfferEntity.getUrl());
}
unifedOfferRepository.saveAll(result);
return String.valueOf(result.size());
}
In that foreach I am getting values like for example:
null JavaDeveloper anyLinkUrl
so only UUID is not generating ids.
have you defined the underline class to implement uuid generator?
in a similar case I used the follwing code that accepts a parameter "rangeName" you can remove
#GeneratedValue(generator="intRange")
#GenericGenerator(name="intRange",
strategy = "imp.framework.jerpBridge.IntRangeGenerator" ,
parameters = {
#Parameter( name = "rangeName", value="DOCMA01")
})
public String nrDocumento;
where
public class IntRangeGenerator implements IdentifierGenerator, Configurable {
public IntRangeGenerator(String rangeName) {
this.rangeName = rangeName;
}
#Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return "generated ID";
}
Given a User entity with the following attributes mapped:
#Entity
#Table(name = "user")
public class User {
//...
#Id
#GeneratedValue
#Column(name = "user_id")
private Long id;
#Column(name = "user_email")
private String email;
#Column(name = "user_password")
private String password;
#Column(name = "user_type")
#Enumerated(EnumType.STRING)
private UserType type;
#Column(name = "user_registered_date")
private Timestamp registeredDate;
#Column(name = "user_dob")
#Temporal(TemporalType.DATE)
private Date dateOfBirth;
//...getters and setters
}
I have created a controller method that returns a user by ID.
#RestController
public class UserController {
//...
#RequestMapping(
value = "/api/users/{id}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> getUser(#PathVariable("id") Long id) {
User user = userService.findOne(id);
if (user != null) {
return new ResponseEntity<User>(user, HttpStatus.OK);
}
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}
//...
}
A service in my business logic layer.
public class UserServiceBean implements UserService {
//...
public User findOne(Long id) {
User user = userRepository.findOne(id);
return user;
}
//...
}
And a repository in my data layer.
public interface UserRepository extends JpaRepository<User, Long> {
}
This works fine, it returns everything about the user, but I use this in several different parts of my application, and have cases when I only want specific fields of the user.
I am learning spring-boot to create web services, and was wondering: Given the current implementation, is there a way of picking the attributes I want to publish in a web service?
If not, what should I change in my implementation to be able to do this?
Thanks.
Firstly, I agree on using DTOs, but if it just a dummy PoC, you can use #JsonIgnore (jackson annotation) in User attributes to avoid serializing them, for example:
#Entity
#Table(name = "user")
public class User {
//...
#Column(name = "user_password")
#JsonIgnore
private String password;
But you can see there, since you are not using DTOs, you would be mixing JPA and Jackson annotations (awful!)
More info about jackson: https://github.com/FasterXML/jackson-annotations