How to validate configuration properties only on certain condition? - spring

I have the following configuration properties class:
#Getter
#Setter
#ConfigurationProperties(prefix = "myprops")
public class MyProps {
private boolean enabled = true;
#NotEmpty
private String hostname;
#NotNull
private Integer port;
}
I want the validation annotations on hostname and port only to be considered when enabled = true. When enabled = false the validation should not be executed.
I have already tried putting the validations inside a validation group named OnEnabled and I tried applying #Validated(OnEnabled.class) in a #Configuration class annotated with #ConditionalOnProperty, but that didn't seem to work:
#Configuration(proxyBeanMethods = false)
#ConditionalOnProperty(name = "myprops.enabled", matchIfMissing = true)
public class MyPropsConfiguration {
#Bean
#Validated(OnEnabled.class)
#ConfigurationProperties(prefix = "myprops")
public MyProps myProps() {
return new MyProps();
}
}
I also tried the following, but it is giving me a compile time error about duplicate configuration property prefixes:
#Configuration(proxyBeanMethods = false)
public class MyPropsAutoConfiguration {
#Configuration(proxyBeanMethods = false)
#ConditionalOnProperty(name = "myprops.enabled", matchIfMissing = true)
public static class MyPropsEnabledConfiguration {
#Bean
#Validated
#ConfigurationProperties(prefix = "myprops")
public MyProps myProps() {
return new MyProps();
}
}
#Configuration(proxyBeanMethods = false)
#ConditionalOnProperty(name = "myprops.enabled", havingValue = "false")
public static class MyPropsDisabledConfiguration {
#Bean
#ConfigurationProperties(prefix = "myprops")
public MyProps myProps() {
return new MyProps();
}
}
}
Moving the #ConfigurationProperties to the properties class got rid of the compile error, but also didn't work as expected
Is there any way to achieve this? I know that a custom Validator might be a solution, but I would be interested if this is possible with pure spring annotations?

Related

Mapstruct - How to convert a DTO String parameter to an Entity object?

I'm new to Mapstruct and I'm trying to understand it properly.
What I want to achieve is converting from a DTO String parameter (carModel) to his Entity, retrieve using Service and Repository.
The problem is that Mapper class generated by Mapstruct is trying to inject the Service class with #Autowired annotation, but it's not working. The service is null.
Here's my #Mapper class:
#Mapper(componentModel = "spring", uses = CarModelService.class)
public interface KitMapper extends EntityMapper<KitDTO, Kit> {
KitMapper INSTANCE = Mappers.getMapper(KitMapper.class);
#Mapping(source = "weight", target = "systemWeight")
#Mapping(source = "carModel", target = "carModel")
Kit toEntity(KitDTO kitDTO);
}
public interface EntityMapper<D, E> {
E toEntity(D dto);
List<E> toEntity(List<D> dtoList);
}
The #Service class:
#Service
#Transactional
public class CarModelService {
private final CarModelRepository carModelRepository;
#Transactional(readOnly = true)
public CarModel findByName(String name) {
return carModelRepository.findByName(name).orElse(null);
}
}
The #Repository class:
#Repository
public interface CarModelRepository extends JpaRepository<CarModel, Long> {
Optional<CarModel> findByName(String carModelName);
}
The DTO and Entity classes:
public class KitDTO {
private String id;
private String carModel; // e.g. "Ferrari Monza"
....
}
#Entity
#Table(name = "kit")
public class Kit implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
private Long id;
#ManyToOne
private CarModel carModel;
...
}
#Entity
#Table(name = "car_model")
public class CarModel implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
...
}
The build work properly but the application stop when I try to use the Mapper. It says that carModelService is null.
Here's the mapper generated implementation class:
#Component
public class KitMapperImpl implements KitMapper {
#Autowired // <-- this seems not working
private CarModelService carModelService;
#Override
public Kit toEntity(KitDTO kitDTO) {
if ( kitDTO == null ) {
return null;
}
Kit kit = new Kit();
kit.setSystemWeight( String.valueOf( kitDTO.getWeight() ) );
kit.carModel( carModelService.findByName(kitDTO.getCarModel()) ); // <-- carModelService is null!
// other setters
return kit;
}
}
I've tried many things, using Decorator, #Context, expression, inject the #Mapper class into the #Service class.
I've found many questions but actually no one helped me:
Mapstruct - How can I inject a spring dependency in the Generated Mapper class
#Service Class Not Autowired in org.mapstruct.#Mapper Class
MapStruct mapper not initialized with autowired when debug
Any help would be appreciated! Thanks in advance!
Found the solution!
Instead of calling directly the Mapper method toEntity() from the #RestController class, I injected the mapper in the CarModelService class and created a method that call the mapper.
In this way the flow is:
Controller --> Service --> Mapper
#Service
#Transactional
public class KitService {
private final KitRepository kitRepository;
private final KitSearchRepository kitSearchRepository;
private final KitMapper kitMapper; // <-- mapper declaration
public KitService(KitRepository kitRepository, KitSearchRepository kitSearchRepository, KitMapper kitMapper) {
this.kitRepository = kitRepository;
this.kitSearchRepository = kitSearchRepository;
this.kitMapper = kitMapper; // <-- mapper initilization
}
// here the method which calls mapper
public Kit convertDTOToEntity(KitDTO kitDTO) {
return kitMapper.toEntity(kitDTO);
}
In this way, the generated class by Mapstruct doesn't give error on the CarModelService.
Seems like this approach is the only way to achieve this, create a king of "bridge" between services and mappers.
(You can use also the #Autowired annotation instead of constructor)
Can you please share the error message?
From the information that you shared, I can see the carModel in KitDto is String and in Entity is CarModel class. Not sure how mapstruct's auto generated implementation class implemented this: kit.carModel( carModelService.findByName(kitDTO.getCarModel()) );.
But I would like to share another approach,Don't know this is a best practice or not. In this approach you can create a abstarct class of mapper, in which you can #Autowired repository can manually add those mapping.
I shared the snippet for it. Hopefully this will help you.
#Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public abstract class ProductMapper {
#Autowired
private CarModelService carModelService;
public abstract Kit convertDTOToEntity(KitDTO kitDTO);
public Kit toEntity(KitDTO kitDTO);
{
Kit kit = convertDTOToEntity(kitDTO);
kit.setCarModel(carModelService.findByName(kitDTO.getCarModel()));
return kit;
}
}
Curious about the other approaches, will follow this thread. We can discuss the best practices

SpringBoot check if injected Properties are set NotNull

I know I can easily inject a property file in SpringBoot 2.2 with the following construct
#ConstructorBinding
#ConfigurationProperties(prefix = "example")
#Data
#AllArgsConstructor
public final class MyProps {
#NonNull
private final String neededProperty;
#NonNull
private final List<SampleProps> lstNeededProperty;
public String getFirstSample(){
return lstNeededProperty.get(0); //throws NPE
}
}
#ConstructorBinding
#AllArgsConstructor
#Data
public class SampleProps {
String key;
String label;
}
and yml file like:
expample:
neededProperty: test1
lstNeededProperty:
-key: abc
label: input
The #NonNull works quite well for the String but fails for the List - since the NPE is thrown even when the list will be set.
Is there a simple way to check if the List is initialized? I've tried #Postconstruct but this isn't called at all.
Try to check for the size and intialize the list:
#ConstructorBinding
#ConfigurationProperties(prefix = "example")
public final class MyProps {
#NonNull
private final String neededProperty;
#Size(min=1)
private final List<String> lstNeededProperty = new ArrayList<>();
}

How to validate objects that were deserialized with Jackson polymorphic deserialization

Given the following request:
/enterprises-api/{{version}}/enterprises?query={"searchType":"FOUNDERSEARCH","maxResult":"61",...}
OR
/enterprises-api/{{version}}/enterprises?query={"searchType":"NAMEORADDRESSSEARCH","maxResult":"61",...}
I have managed to deserialize query into either a FounderSearch or NameOrAddressSearch object based on the searchType property.
However, Javax validation is ignored because this isn't a toplevel object. How can I resolve this?
Top level object:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "searchType")
#JsonSubTypes({
#JsonSubTypes.Type(value = FounderSearch.class, name = "FOUNDERSEARCH"),
#JsonSubTypes.Type(value = NamerOrAddressSearch.class, name = "NAMEORADDRESSSEARCH")
})
public abstract class Query {
public abstract boolean isFounderSearch();
}
One of the sub-level objects:
#Data
public class FounderSearch extends QueryObject {
#Min(1) #Max(60)
private Integer maxResult = 20;
#Pattern(regexp = "\\d{11}")
private String personNumber;
private List<CodeType> functions;
private Boolean activeFunctions;
public boolean isFounderSearch() {
return true;
}
public SearchType getSearchType() {
return SearchType.FOUNDERSEARCH;
}
}
If you add #Valid annotation on nested objects as well it will work.
something like this, above all the fields that have validation constraints.
#Data
public class FounderSearch extends QueryObject {
#Min(1) #Max(60)
#Valid
private Integer maxResult = 20;
#Pattern(regexp = "\\d{11}")
#Valid
private String personNumber;
private List<CodeType> functions;
private Boolean activeFunctions;
public boolean isFounderSearch() {
return true;
}
public SearchType getSearchType() {
return SearchType.FOUNDERSEARCH;
}
}

Spring Data JPA: using property in #Query as parameter

I have several application-x.properties files which are used for different profiles and each file contains a property with a specific value for each profile. I want to use this property in queries to database as a parameter.
Is it possible to add it using SpEL or something else?
For instance application.properties:
query.parameters.role: ADMIN
possible usage:
#Query(value = "select u from User u where u.role = :#{query.parameters.role}")
Set<User> getAllUsers();
You might do it by following way:
1.- Find all users by role using Repository Query Keywords
#Repository
public interface UserRepository extends JpaRepository<User, UUID> {
Set<User> findByRole(String role);
}
2.- Create a method called getAllUsers in UserService and get role value by using #Value:
#Service
public class UserService {
#Autowired
private UserRepository repository;
#Value("${query.parameters.role}")
private String role;
public Set<User> getAllUsers() {
return repository.findByRole(role);
}
}
Other way to answer to this question is implement a custom SpEL that is supported by #Query you can take a look this SpEL support in Spring Data JPA
Then you should follow these steps for your case:
1.- Create a ConfigProperties class so that you can read and get the application.properties.
#Configuration
#PropertySource("classpath:application.properties")
#ConfigurationProperties(prefix = "query")
public class ConfigProperties {
private Parameters parameters;
// standard getters and setters
}
public class Parameters {
private String role;
// standard getters and setters
}
2.- Implement a custom EvaluationContextExtensionSupport and reference to ConfigProperties
public class PropertyEvaluationContextExtension extends EvaluationContextExtensionSupport {
private final ConfigProperties configProperties;
public PropertyEvaluationContextExtension(final ConfigProperties configProperties) {
this.configProperties= configProperties;
}
#Override
public String getExtensionId() {
return "properties";
}
#Override
public ConfigProperties getRootObject() {
return this.configProperties;
}
}
3.- Create a bean in order to be called your custom PropertyEvaluationContextExtension
#Configuration
public class CustomConfig {
private final ConfigProperties configProperties;
public CustomConfig(final ConfigProperties configProperties) {
this.configProperties= configProperties;
}
#Bean
EvaluationContextExtensionSupport propertyExtension() {
return new PropertyEvaluationContextExtension(configProperties);
}
}
4.- Call the query.parameters.role by following format: ?#{query.parameters.role}
#Query(value = "SELECT u FROM User u WHERE u.role = ?#{query.parameters.role}")
Set<User> getAllUsers();

Why is the child collection is null in One-To-Many relationship of spring boot application?

I create a spring boot application with MySQL,JPA,Web dependencies,and manually config my database settings in .properties file of Spring boot. I passed compiling, and started application successfully, and adding one record is normal fine.
BUT, i use method 'findAll(Pageable pageable)' i got a problem, that was
Could not write JSON: failed to lazily initialize a collection of roleļ¼Œcould not initialize proxy - no Session
I got confused, i started to debug my code, finally i found that the child collection of the result is null, and it contained an error, which is
"Exception occurred: com.sun.jdi.InvocationException occurred invoking method.."
I tried a lot to fix my code, but no use.
who can help me?
The entity relationship is a simple one to many:
TeacherInfo entity and ClassInfo entity, teacher manage multiple classes, just simple as this.
here is the enter point of my app:
#SpringBootApplication(exclude= {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
DataSourceAutoConfiguration.class
})
#EnableTransactionManagement
public class OrmTestApplication {
public static void main(String[] args) {
SpringApplication.run(OrmTestApplication.class, args);
}
}
Database properties setting is here:
spring.datasource.primary.url=jdbc:mysql://localhost:3306/ormtest?useSSL=false
spring.datasource.primary.username=root
spring.datasource.primary.password=BlaNok2700
spring.datasource.primary.driver-class-name = com.mysql.jdbc.Driver
hibernate.hbm2ddl.auto = update
hibernate.show-sql = true
My Data base configure java code is here:
Configuration
#EnableJpaRepositories(basePackages = "com.lanjian.ormtest.repositories", entityManagerFactoryRef = "primaryEntityManagerFactory", transactionManagerRef = "primaryTransactionManager")
public class PrimaryDbConfig {
#Autowired
private Environment env;
#Bean
#ConfigurationProperties(prefix="spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
#Bean
public DataSource primaryDataSource() {
DataSourceProperties dbProperty = primaryDataSourceProperties();
return DataSourceBuilder.create()
.driverClassName(dbProperty.getDriverClassName())
.url(dbProperty.getUrl())
.username(dbProperty.getUsername())
.password(dbProperty.getPassword())
.build();
}
#Bean
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setDataSource(primaryDataSource());
factory.setPackagesToScan("com.lanjian.ormtest.entities");
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties jpaProperties = new Properties();
jpaProperties.put("hibernate.hbm2ddl.auto", env.getProperty("hibernate.hbm2ddl.auto"));
jpaProperties.put("hibernate.show-sql", env.getProperty("hibernate.show-sql"));
factory.setJpaProperties(jpaProperties);
return factory;
}
#Bean
public PlatformTransactionManager primaryTransactionManager() {
EntityManagerFactory factory = primaryEntityManagerFactory().getObject();
return new JpaTransactionManager(factory);
}
}
My REST controller method is here:
#Autowired
private TeacherRepository teacherRepository;
#GetMapping("/page")
public Page<TeacherInfo> page(Pageable pageable){
Page<TeacherInfo> list = teacherRepository.findAll(pageable);
return list;
}
What happened
After i started my application, and use postman send request, i got this:
got a 500 error
And i debugger my code, found this:
child collection is null
In the picture, 'classes' is a list collection, but it is null, i don't understand.
Here are the TeacherInfo entity I defined
#Entity
#Table(name = "teacher")
public class TeacherInfo {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private byte age;
private boolean male;
#OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY, mappedBy="chargedTeacher")
private List<ClassInfo> classes = new ArrayList<>();
public void initialize() {
for (ClassInfo classInfo : classes) {
classInfo.setChargedTeacher(this);
for (StudentInfo studentInfo : classInfo.getStudents()) {
studentInfo.setClassInfo(classInfo);
}
}
}
//Setters and Getters}
Here is the ClassInfo Entity i defined
#Entity
#Table(name = "class_info")
public class ClassInfo {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int capacity;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "teacher_id",nullable=false)
#JsonIgnore
private TeacherInfo chargedTeacher;
#OneToMany(cascade = CascadeType.ALL,fetch=FetchType.LAZY,mappedBy="classInfo")
private List<StudentInfo> students = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public TeacherInfo getChargedTeacher() {
return chargedTeacher;
}
public void setChargedTeacher(TeacherInfo chargedTeacher) {
this.chargedTeacher = chargedTeacher;
}
public List<StudentInfo> getStudents() {
return students;
}
public void setStudents(List<StudentInfo> students) {
this.students = students;
}
}
I think that the problem may come from Transactionality and JPA Fetching types.
Your repository method is being invoked not using a transaction, which implies that the transaction is on the boundaries of the method invocation (which might not be wrong). Spring returns a Page with objects but when it tries to serialize them, transaction is gone so no way to access childs.
I would suggest to put the JPA relationship as EAGER fetching, allowing all the objects to be present on the repository result when the transaction ends.
EDIT:
Answer to comments
#Bean
public PlatformTransactionManager primaryTransactionManager(EntityManagerFactory factory) {
return new JpaTransactionManager(factory);
}

Resources