I have a field, say, user_name, that should be unique in a table.
What is the best way for validating it using Spring/Hibernate validation?
One of the possible solutions is to create custom #UniqueKey constraint (and corresponding validator); and to look-up the existing records in database, provide an instance of EntityManager (or Hibernate Session)to UniqueKeyValidator.
EntityManagerAwareValidator
public interface EntityManagerAwareValidator {
void setEntityManager(EntityManager entityManager);
}
ConstraintValidatorFactoryImpl
public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {
private EntityManagerFactory entityManagerFactory;
public ConstraintValidatorFactoryImpl(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
T instance = null;
try {
instance = key.newInstance();
} catch (Exception e) {
// could not instantiate class
e.printStackTrace();
}
if(EntityManagerAwareValidator.class.isAssignableFrom(key)) {
EntityManagerAwareValidator validator = (EntityManagerAwareValidator) instance;
validator.setEntityManager(entityManagerFactory.createEntityManager());
}
return instance;
}
}
UniqueKey
#Constraint(validatedBy={UniqueKeyValidator.class})
#Target({ElementType.TYPE})
#Retention(RUNTIME)
public #interface UniqueKey {
String[] columnNames();
String message() default "{UniqueKey.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Target({ ElementType.TYPE })
#Retention(RUNTIME)
#Documented
#interface List {
UniqueKey[] value();
}
}
UniqueKeyValidator
public class UniqueKeyValidator implements ConstraintValidator<UniqueKey, Serializable>, EntityManagerAwareValidator {
private EntityManager entityManager;
#Override
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
private String[] columnNames;
#Override
public void initialize(UniqueKey constraintAnnotation) {
this.columnNames = constraintAnnotation.columnNames();
}
#Override
public boolean isValid(Serializable target, ConstraintValidatorContext context) {
Class<?> entityClass = target.getClass();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root<?> root = criteriaQuery.from(entityClass);
List<Predicate> predicates = new ArrayList<Predicate> (columnNames.length);
try {
for(int i=0; i<columnNames.length; i++) {
String propertyName = columnNames[i];
PropertyDescriptor desc = new PropertyDescriptor(propertyName, entityClass);
Method readMethod = desc.getReadMethod();
Object propertyValue = readMethod.invoke(target);
Predicate predicate = criteriaBuilder.equal(root.get(propertyName), propertyValue);
predicates.add(predicate);
}
} catch (Exception e) {
e.printStackTrace();
}
criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()]));
TypedQuery<Object> typedQuery = entityManager.createQuery(criteriaQuery);
List<Object> resultSet = typedQuery.getResultList();
return resultSet.size() == 0;
}
}
Usage
#UniqueKey(columnNames={"userName"})
// #UniqueKey(columnNames={"userName", "emailId"}) // composite unique key
//#UniqueKey.List(value = {#UniqueKey(columnNames = { "userName" }), #UniqueKey(columnNames = { "emailId" })}) // more than one unique keys
public class User implements Serializable {
private String userName;
private String password;
private String emailId;
protected User() {
super();
}
public User(String userName) {
this.userName = userName;
}
....
}
Test
public void uniqueKey() {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("default");
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
ValidatorContext validatorContext = validatorFactory.usingContext();
validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(entityManagerFactory));
Validator validator = validatorContext.getValidator();
EntityManager em = entityManagerFactory.createEntityManager();
User se = new User("abc", poizon);
Set<ConstraintViolation<User>> violations = validator.validate(se);
System.out.println("Size:- " + violations.size());
em.getTransaction().begin();
em.persist(se);
em.getTransaction().commit();
User se1 = new User("abc");
violations = validator.validate(se1);
System.out.println("Size:- " + violations.size());
}
I think it is not wise to use Hibernate Validator (JSR 303) for this purpose.
Or better it is not the goal of Hibernate Validator.
The JSR 303 is about bean validation. This means to check if a field is set correct. But what you want is in a much wider scope than a single bean. It is somehow in a global scope (regarding all Beans of this type). -- I think you should let the database handle this problem. Set a unique constraint to the column in your database (for example by annotate the field with #Column(unique=true)) and the database will make sure that the field is unique.
Anyway, if you really want to use JSR303 for this, than you need to create your own Annotation and own Validator. The Validator have to access the Database and check if there is no other entity with the specified value. - But I believe there would be some problems to access the database from the Validator in the right session.
One possibility is to annotate the field as #NaturalId
You could use the #Column attribute which can be set as unique.
I've found kind of a tricky solution.
First, I've implemented the unique contraint to my MySql database :
CREATE TABLE XMLTAG
(
ID INTEGER NOT NULL AUTO_INCREMENT,
LABEL VARCHAR(64) NOT NULL,
XPATH VARCHAR(128),
PRIMARY KEY (ID),
UNIQUE UQ_XMLTAG_LABEL(LABEL)
) ;
You see that I manage XML Tags that are defined by a unique label and a text field named "XPath".
Anyway, the second step is to simply catch the error raised when the user tries to do a bad update. A bad update is when trying to replace the current label by an existing label. If you leave the label untouched, no problemo. So, in my controller :
#RequestMapping(value = "/updatetag", method = RequestMethod.POST)
public String updateTag(
#ModelAttribute("tag") Tag tag,
#Valid Tag validTag,
BindingResult result,
ModelMap map) {
if(result.hasErrors()) { // you don't care : validation of other
return "editTag"; // constraints like #NotEmpty
}
try {
tagService.updateTag(tag); // try to update
return "redirect:/tags"; // <- if it works
}
catch (DataIntegrityViolationException ex) { // if it doesn't work
result.rejectValue("label", "Unique.tag.label"); // pass an error message to the view
return "editTag"; // same treatment as other validation errors
}
}
This may conflict with the #Unique pattern but you can use this dirty method to valid the adding too.
Note : there is still one problem : if other validation errors are catched before the exception, the message about unicity will not be displayed.
This code is based on the previous one implemented using EntityManager.
In case anyone need to use Hibernate Session.
Custom Annotation using Hibernate Session.
#UniqueKey.java
import java.lang.annotation.*;
import javax.validation.Constraint;
import javax.validation.Payload;
#Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UniqueKeyValidator.class)
#Documented
public #interface UniqueKey {
String columnName();
Class<?> className();
String message() default "{UniqueKey.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
UnqieKeyValidator.java
import ch.qos.logback.classic.gaffer.PropertyUtil;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
#Transactional
#Repository
public class UniqueKeyValidator implements ConstraintValidator<UniqueKey, String> {
#Autowired
private SessionFactory sessionFactory;
public Session getSession() {
return sessionFactory.getCurrentSession();
}
private String columnName;
private Class<?> entityClass;
#Override
public void initialize(UniqueKey constraintAnnotation) {
this.columnNames = constraintAnnotation.columnNames();
this.entityClass = constraintAnnotation.className();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Class<?> entityClass = this.entityClass;
System.out.println("class: " + entityClass.toString());
Criteria criteria = getSession().createCriteria(entityClass);
try {
criteria.add(Restrictions.eq(this.columnName, value));
} catch (Exception e) {
e.printStackTrace();
}
return criteria.list().size()==0;
}
}
Usage
#UniqueKey(columnNames="userName", className = UserEntity.class)
// #UniqueKey(columnNames="userName") // unique key
Related
I have a problem that I've solved but I don't understand why. so maybe someone can help me to understand it. I have create a custom constrain for hibernate validator. when I operate the entity, using update, merge or saveorupdate. it always return stack trace overflow exception. so I google it and then I try some code, than it worked. but I still want to know why it worked and why it throws the exception.
this is the code for custom annotation
#Target({METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = UniqueIDValidator.class)
#Documented
public #interface UniqueKey {
String message() default "{validation.unique}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* The mapped hibernate/jpa entity class
*/
Class<?> entity();
/**
* The property of the entity we want to validate for uniqueness. Default name is "id"
*/
String property() default "id";
}
and this is the custom constraint validator code
public class UniqueIDValidator implements ConstraintValidator<UniqueKey, Serializable>{
private final static Logger LOG= Logger.getLogger(UniqueIDValidator.class);
#Autowired
private SessionFactory sessionFactory;
private Class<?> entityClass;
private String uniqueField;
public void initialize(UniqueKey unique) {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
entityClass = unique.entity();
uniqueField = unique.property();
}
#Override
public boolean isValid(Serializable arg0, ConstraintValidatorContext arg1) {
String query = String.format("from %s where %s = :%s ", entityClass.getName(), uniqueField, uniqueField);
List<?> list = null;
try{
list = sessionFactory.getCurrentSession().createQuery(query).setParameter(uniqueField, arg0).list(); <---- This line cause the exeption
}catch(Exception e){
LOG.error(e);
}
return (list != null && !(list.size() > 1));
}
}
after i follow Here I try to wired the EntityManager but always return null, so I have this code below, and it worked.
#Override
public boolean isValid(Serializable arg0, ConstraintValidatorContext arg1) {
String query = String.format("from %s where %s = :%s ", entityClass.getName(), uniqueField, uniqueField);
List<?> list = null;
try{
// below line of code make it worked
sessionFactory.getCurrentSession().setFlushMode(FlushMode.COMMIT);
list = sessionFactory.getCurrentSession().createQuery(query).setParameter(uniqueField, arg0).list();
}catch(Exception e){
LOG.error(e);
}finally {
// this is to reset the hibernate config I think
sessionFactory.getCurrentSession().setFlushMode(FlushMode.AUTO);
}
return (list != null && !(list.size() > 1));
}
the exception is
java.lang.StackOverflowError at java.util.HashMap$EntrySet.iterator(HashMap.java:1082) at sun.reflect.annotation.AnnotationInvocationHandler.hashCodeImpl(AnnotationInvocationHandler.java:294)
at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:64)
at com.sun.proxy.$Proxy35.hashCode(Unknown Source) at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager$CacheKey.createHashCode(ConstraintValidatorManager.java:326)
What I want to ask is:
1. why before I add Flushmode.COMMIT it throws stackoverflow exception?
2. Is this change will affect others hibernate behaviour such as save, persist etc?
I'm sorry for my poor english, english is my not my native language. Thanks in advance.
Now i have new problem, this constraint is evaluated when I do update. I have to check if the object is in dirty state or not. but I don't know how.
I have a very basic JAX-RS service (the BookService class below) which allows for the creation of entities of type Book (also below). POSTing the payload
{
"acquisitionDate": 1418849700000,
"name": "Funny Title",
"numberOfPages": 100
}
successfully persists the Book and returns 201 CREATED. However, including an id attribute with whichever non-null value on the payload triggers an org.hibernate.PersistentObjectException with the message detached entity passed to persist. I understand what this means, and including an id on the payload when creating an object (in this case) makes no sense. However, I'd prefer to prevent this exception from bubbling all the way up and present my users with, for instance, a 400 BAD REQUEST in this case (or, at least, ignore the attribute altogether). However, there are two main concerns:
The exception that arrives at create is an EJBTransactionRolledbackException and I'd have to crawl all the way down the stack trace to discover the root cause;
The root cause is org.hibernate.PersistentObjectException - I'm deploying to Wildfly which uses Hibernate, but I want to maintain my code portable, so I don't really want to catch this specific exception.
To my understanding, there are two possible solutions:
Use book.setId(null) before bookRepo.create(book). This would ignore the fact that the id attribute carries a value and proceed with the request.
Check if book.getId() != null and throw something like IllegalArgumentException that could be mapped to a 400 status code. Seems the preferable solution.
However, coming from other frameworks (like Django Rest Framework, for example) I'd really prefer this to be handled by the framework itself... My question then is, is there any built-in way to achieve this behaviour that I may be missing?
This is the BookService class:
#Stateless
#Path("/books")
public class BookService {
#Inject
private BookRepo bookRepo;
#Context
UriInfo uriInfo;
#Consumes(MediaType.APPLICATION_JSON)
#Path("/")
#POST
#Produces(MediaType.APPLICATION_JSON)
public Response create(#Valid Book book) {
bookRepo.create(book);
return Response.created(getBookUri(book)).build();
}
private URI getBookUri(Book book) {
return uriInfo.getAbsolutePathBuilder()
.path(book.getId().toString()).build();
}
}
This is the Book class:
#Entity
#Table(name = "books")
public class Book {
#Column(nullable = false)
#NotNull
#Temporal(TemporalType.TIMESTAMP)
private Date acquisitionDate;
#Column(nullable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Id
private Integer id;
#Column(nullable = false)
#NotNull
#Size(max = 255, min = 1)
private String name;
#Column(nullable = false)
#Min(value = 1)
#NotNull
private Integer numberOfPages;
(getters/setters/...)
}
This is the BookRepo class:
#Stateless
public class BookRepo {
#PersistenceContext(unitName = "book-repo")
protected EntityManager em;
public void create(Book book) {
em.persist(book);
}
}
I don't know if this is really the answer you're looking for, but I was just playing around with the idea and implemented something.
The JAX-RS 2 spec defines a model for bean validation, so I thought maybe you could tap into that. All bad validations will get mapped to a 400. You stated "I'd prefer to prevent this exception from bubbling all the way up and present my users with, for instance, a 400 BAD REQUEST", but with bad validation you will get that anyway. So however you plan to handle validation exceptions (if at all), you can do the same here.
Basically I just created a constraint annotation to validate for a null value in the id field. You can define the id field's name in the annotation through the idField annotation attribute, so you are not restricted to id. Also this can be used for other objects too, so you don't have to repeatedly check the value, as you suggested in your second solution.
You can play around with it. Just thought I'd throw this option out there.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
#Constraint(validatedBy = NoId.NoIdValidator.class)
#Target({ElementType.PARAMETER})
#Retention(RUNTIME)
public #interface NoId {
String message() default "Cannot have value for id attribute";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String idField() default "id";
public static class NoIdValidator implements ConstraintValidator<NoId, Object> {
private String idField;
#Override
public void initialize(NoId annotation) {
idField = annotation.idField();
}
#Override
public boolean isValid(Object bean, ConstraintValidatorContext cvc) {
boolean isValid = false;
try {
Field field = bean.getClass().getDeclaredField(idField);
if (field == null) {
isValid = true;
} else {
field.setAccessible(true);
Object value = field.get(bean);
if (value == null) {
isValid = true;
}
}
} catch (NoSuchFieldException
| SecurityException
| IllegalArgumentException
| IllegalAccessException ex) {
Logger.getLogger(NoId.class.getName()).log(Level.SEVERE, null, ex);
}
return isValid;
}
}
}
Usage:
#POST
#Consumes(MediaType.APPLICATION_JSON)
public Response createBook(#Valid #NoId(idField = "id") Book book) {
book.setId(1);
return Response.created(URI.create("http://blah.com/books/1"))
.entity(book).build();
}
Note the default idField is id, so if you don't specify it, it will look for the id field in the object class. You can also specify the message as you would any other constraint annotation:
#NoId(idField = "bookId", message = "bookId must not be specified")
// default "Cannot have value for id attribute"
suppose i have the following scenario:
public class EntityA {
private List<EntityB> listOfBs;
}
im trying to cascade validation to the list of Bs only if running under a certain validation group. so ideally, this:
public class EntityA {
#Valid(groups = {SomeSpecificGroup.class})
private List<EntityB> listOfBs;
}
unfortunately, #Valid does not have a groups() property. so i figured i'd try something like:
#Constraint(validatedBy = { CascadedValidator.class })
#Target({ ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface CascadedValidation {
Class<?>[] groups() default { };
}
and write a validator (CascadedValidator) that upon activation will do the cascade (==will validate all elements of the collection its placed on).
my issue is how do i perform the cascaded validation?
so far i have this:
public class CascadedValidator implements ConstraintValidator<CascadedValidation, Object>{
private Class<?>[] groups;
#Override public void initialize(CascadedValidation constraintAnnotation) {
groups = constraintAnnotation.groups();
}
#Override public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null || !(value instanceof Iterable)) {
return true;
}
for (Object item : (Iterable)value) {
//validate item using the groups?!
}
}
}
i know i could implement the actual validation by creating another Validator "inline":
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations;
if (decideIfCascade(groups)) {
for (Object item : (Iterable)value) {
if (groups!=null && groups.length>0) {
violations = validator.validate(item, groups);
} else {
violations = validator.validate(item);
}
if (!violations.isEmpty()) {
return false;
}
}
}
return true;
but this just smells bad to me.
surely there's a sane/normal/easy way of doing this?
EDIT - the actual use case
my API accept both EntityA (which has a list of Bs) and EntityB as top-level entities (so you can send a single B directly). both A and B have an id property, but i only require a non-null id on the top level object submitted. so if the service gets an A with an idea and several "blank" Bs its ok, but if i get a B as a top level parameter it must have an id.
You should not invoke the validation engine from within a ConstraintValidator implementation.
If you are on Bean Validation 1.1, have a look at group conversions which give you control over the validation groups propagated upon cascaded validation. E.g. you could do the following:
#Valid
#ConvertGroup(from = Default.class, to = SomeSpecificGroup.class)
private List<EntityB> listOfBs;
I want to add annotations on my classes properties, and then iterate all my properties with the ability to lookup the annotations also.
So for example, I have a class like:
public class User {
#Annotation1
private int id;
#Annotation2
private String name;
private int age;
// getters and setters
}
Now I want to be able to loop through my properties, and be able to know what annotation (if any) is on the property.
I want to know how to do this using just java, but also curious if using either spring, guava or google guice would make this any easier (if they have any helpers to do this easier).
Here is an example that utilizes the (barely maintained) bean instrospection framework. It's an all Java solution that you can extend to fit your needs.
public class BeanProcessor {
public static void main(String[] args) {
try {
final Class<?> beanClazz = BBean.class;
BeanInfo info = Introspector.getBeanInfo(beanClazz);
PropertyDescriptor[] propertyInfo = info.getPropertyDescriptors();
for (final PropertyDescriptor descriptor : propertyInfo) {
try {
final Field field = beanClazz.getDeclaredField(descriptor
.getName());
System.out.println(field);
for (final Annotation annotation : field
.getDeclaredAnnotations()) {
System.out.println("Annotation: " + annotation);
}
} catch (final NoSuchFieldException nsfe) {
// ignore these
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Below is the way to create your own annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.TYPE)
public #interface Annotation1 {
public String name();
public String value();
}
After defining your annotation, use the annotation as you mentioned in your question and you can use the below reflection method to get the annotated class details
Class aClass = User.class;
Annotation[] annotations = aClass.getAnnotations();
for(Annotation annotation : annotations){
if(annotation instanceof Annotation1){
Annotation1 myAnnotation = (Annotation1) annotation;
System.out.println("name: " + myAnnotation.name());
System.out.println("value: " + myAnnotation.value());
}
}
I created the method below which creates a stream of all fields in a class and it's superclasses which have a specific annotation.
There are other ways to do it. But I think this solution is very easy to reuse and practical because when you need to know those fields, it is usually to do an action on each field. And a Stream is exactly what you need to do that.
public static Stream<Field> getAnnotatedFieldStream(Class<?> theClass, Class<? extends Annotation> annotationType) {
Class<?> classOrSuperClass = theClass;
Stream<Field> stream = Stream.empty();
while(classOrSuperClass != Object.class) {
stream = Stream.concat(stream, Stream.of(classOrSuperClass.getDeclaredFields()));
classOrSuperClass = classOrSuperClass.getSuperclass();
}
return stream.filter(f -> f.isAnnotationPresent(annotationType));
}
you would use reflection to get the fields of the class and then call something like getAnnotations() on each field.
I work on an application that has been converted from pure JDBC to Spring template with row mapper. The issue that I have is that the column in database doesn't match the property names which prevent me from using BeanPropertyRowMapper easily.
I saw some posts about using aliases in queries. This would work but it makes it impossible to do a SELECT *
Isn't there any annotation that can be used with BeanPropertyRowMapper as #Column from JPA?
I saw Some posts about using aliases in queries
This is actually an approach suggested in JavaDocs:
To facilitate mapping between columns and fields that don't have matching names, try using column aliases in the SQL statement like "select fname as first_name from customer".
From: BeanPropertyRowMapper.
impossible to do a SELECT *
Please do not use SELECT *. This makes you vulnerable to any database schema change, including completely backward compatible ones like adding or rearranging columns.
Isn't there any annotation that can be used with BeanPropertyRowMapper as #Column from JPA?
Yes, it is called jpa, hibernate and maybe ibatis. Seriously, either use aliases or implement your own RowMapper, Spring is not a full-featured orm.
You can override the BeanPropertyRowMapper.underscoreName, and get the name of the Column annotation to mapping the field with #Column(name = "EXAMPLE_KEY") in the PropertyDescriptor(getter/setter binding).
#Slf4j
public class ColumnRowMapper<T> extends BeanPropertyRowMapper<T> {
private ColumnRowMapper(final Class<T> mappedClass)
{
super(mappedClass);
}
#Override
protected String underscoreName(final String name)
{
final Column annotation;
final String columnName;
Field declaredField = null;
try
{
declaredField = getMappedClass().getDeclaredField(name);
}
catch (NoSuchFieldException | SecurityException e)
{
log.warn("Ups, field «{}» not found in «{}».", name, getMappedClass());
}
if (declaredField == null || (annotation = declaredField.getAnnotation(Column.class)) == null
|| StringUtils.isEmpty(columnName = annotation.name()))
{
return super.underscoreName(name);
}
return StringUtils.lowerCase(columnName);
}
/**
* New instance.
*
* #param <T> the generic type
* #param mappedClass the mapped class
* #return the bean property row mapper
*/
public static <T> BeanPropertyRowMapper<T> newInstance(final Class<T> mappedClass)
{
return new ColumnRowMapper<>(mappedClass);
}
}
A version of above mapper but with early initiation of mapping index, since reflection is way too slow:
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.persistence.Column;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
#Slf4j
public class ColumnRowMapper<T> extends BeanPropertyRowMapper<T> {
private Map<String, String> columnIndex;
private ColumnRowMapper(final Class<T> mappedClass)
{
super(mappedClass);
}
#Override
protected void initialize(Class<T> mappedClass) {
columnIndex = new ConcurrentHashMap<>();
for (Field f: mappedClass.getDeclaredFields()) {
String fieldName = f.getName();
Column annotation = f.getAnnotation(Column.class);
if (annotation == null) {
continue;
}
String columnName = annotation.name();
if (StringUtils.isEmpty(columnName)) {
continue;
}
columnIndex.put(fieldName, StringUtils.lowerCase(columnName));
}
super.initialize(mappedClass);
}
#Override
protected #NonNull String underscoreName(final #NonNull String name)
{
if (columnIndex.containsKey(name)) {
return columnIndex.get(name);
}
return super.underscoreName(name);
}
/**
* New instance.
*
* #param <T> the generic type
* #param mappedClass the mapped class
* #return the bean property row mapper
*/
public static <T> BeanPropertyRowMapper<T> newInstance(final Class<T> mappedClass)
{
return new ColumnRowMapper<>(mappedClass);
}
}