Spring/initBinder - Not able to get the value binded when multiple values are selected - spring

The below code works fine when I try to save a single value of miniApple. I can see the value of miniApple object value in argument of controller i.e., apple. However I am not able to binder if there are multiple values. Not sure what should be the syntax to write inside the initBinder method for converting the string (comma separated value) into List of custom objects(here List of MiniApple)
When I select multiple values, I can the text value in setAsText() as comma separated values( eg: "miniApple1, miniApple2,miniAPlle3")
Controller method save()
public ModelAndView save(#ModelAttribute("Apple") Apple apple, BindingResult result, SessionStatus status) {
}
Init Binder method
#InitBinder
public void initBinder(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(MiniApple.class, new MiniAppleEditor(this.miniAppleService, true));
}
Custom Editor class with setAsText method
public class MiniAppleEditor extends PropertyEditorSupport {
/** The mini apple service. */
private final MiniAppleService miniAppleService;
/** Whether or not to allow null values. */
private boolean allowEmpty;
/**
* #param miniAppleService the miniAppleService to set
*/
public MiniAppleEditor(MiniAppleService miniAppleService) {
this(miniAppleService, false);
}
/**
* #param miniAppleService the miniAppleService to set
* #param allowEmpty indicates to allow empty mini apple.
*/
public MiniAppleEditor(MiniAppleService miniAppleService, boolean allowEmpty) {
this.miniAppleService = miniAppleService;
this.allowEmpty = allowEmpty;
}
/**
* #param text the id representing the MiniApple object.
*/
#Override
public void setAsText(String text) {
if (allowEmpty && !StringUtils.hasLength(text)) {
setValue(null);
} else {
setValue(miniAppleService.getMiniApple(Integer.parseInt(text)));
}
}
/**
* #return the Id as a String
*/
#Override
public String getAsText() {
MiniApple miniApple = (MiniApple) getValue();
if (miniApple == null) {
return "";
} else {
return miniApple.getAppleName();
}
}
}
Object which will get the values binded as an argument of controller save () method
#Entity
#Table("tbl_apple")
public class Apple implements Serializable {
private MiniApple miniApple;
getter/setter;
}

Related

What are the best practice for audit log(user activity) in micro-services?

In our microservice architecture, we are logging user-activity to mongo database table? Is there any good way to store and retrieve audit log?
You can think of a solution something similar to the below by storing AuditLogging into the Mongo db by using DAO pattern.
#Entity
#Table(name = "AuditLogging")
public class AuditLogging implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "auditid", updatable = false, nullable = false)
private Long auditId;
#Column(name = "event_type", length = 100)
#Enumerated(EnumType.STRING)
private AuditingEvent event;
#Column(name = "event_creator", length = 100)
#Enumerated(EnumType.STRING)
private EventCreator eventCreator;
#Column(name = "adminid", length = 20)
private String adminId;
#Column(name = "userid", length = 20)
private String userId;
#Column(name = "event_date")
private Date eventDate;
}
public class Constants {
public static final String EVENT_TYPE = "eventType";
public static final String EVENT_CREATOR = "eventCreator";
public static final String NEW_EMAIL_ID = "newEmailId";
public static final String OLD_EMAIL_ID = "oldEmailId";
}
public enum AuditEvent {
USER_REGISTRATION,
USER_LOGIN,
USER_LOGIN_FAIL,
USER_ACCOUNT_LOCK,
USER_LOGOFF,
USER_PASSWORD_CHANGE,
USER_PASSWORD_CHANGE_FAIL,
USER_FORGOT_PASSWORD,
USER_FORGOT_PASSWORD_FAIL,
ADMIN_LOGIN
}
public enum EventCreator {
ADMIN_FOR_SELF,
USER_FOR_SELF,
ADMIN_FOR_USER
}
public interface AuditingDao {
/**
* Stores the event into the DB/Mongo or Whatever
*
* #param auditLogging
* #return Boolean status
*/
Boolean createAuditLog(final AuditLogging auditLogging);
/* Returns the log occurrence of a specific event
*
* #param event
* #return List of logged events by type
*/
List<AuditLogging> getLogsForEvent(final AuditingEvent event);
}
public interface AuditingService {
/**
* Creates an Audit entry in the AuditLogging table using the
* DAO layer
*
* #param auditingEvent
* #param eventCreator
* #param userId
* #param adminId *
* #return {#link Boolean.TRUE} for success and {#link Boolean.FALSE} for
* failure
*/
Boolean createUserAuditEvent(final AuditEvent auditEvent,
final EventCreator eventCreator, final String userId, final String adminId,
final String newEmailId,final String oldEmailId);
/**
*
* Returns all event for a user/admin based on the id
*
* #param id
* #return List of logged events for an id
*/
List<AuditLogging> fetchLoggedEventsById(final String id);
/***
* Returns all event based on event type
*
* #param eventName
* #return List of logged events for an event
*/
List<AuditLogging> fetchLoggedEventsByEventName(final String eventName);
}
#Service("auditingService")
public class AuditServiceImpl implements AuditingService {
#Autowired
private AuditingDao auditingDao;
private static Logger log = LogManager.getLogger();
#Override
public Boolean createUserAuditingEvent(AuditEvent auditEvent,
EventCreator eventCreator, String userId, String adminId,
String newEmailId,String oldEmailId) {
AuditLogging auditLogging = new AuditLogging();
auditLogging.setEvent(auditingEvent);
auditLogging.setEventCreator(eventCreator);
auditLogging.setUserId(userId);
auditLogging.setAdminId(adminId);
auditLogging.setEventDate(new Date());
return Boolean.TRUE;
}
#Override
public List<AuditLogging> fetchLoggedEventsByEventName(
final String eventName) {
AuditEvent event = null;
try {
event = AuditingEvent.valueOf(eventName);
} catch (Exception e) {
log.error(e);
return Collections.emptyList();
}
return auditingDao.getLogsForEvent(event);
}
public void setAuditingDao(AuditingDao auditingDao) {
this.auditingDao = auditingDao;
}
}
Writing an aspect is always good for this type of scenarios by pointing to the appropriate controller method to trigger the event.
#Aspect
#Component("auditingAspect")
public class AuditingAspect {
#Autowired
AuditingService auditingService;
/* The below controllers you can think of your microservice endpoints
*/
#Pointcut("execution(* com.controller.RegistrationController.authenticateUser(..)) ||execution(* com.controller.RegistrationController.changeUserPassword(..)) || execution(* com.controller.RegistrationController.resetPassword(..)) ||execution(* com.controller.UpdateFunctionalityController.updateCustomerDetails(..))")
public void aroundPointCut() {}
#Around("aroundPointCut()")
public Object afterMethodInControllerClass(ProceedingJoinPoint joinPoint)
throws Throwable {
joinPoint.getSignature().getName();
joinPoint.getArgs();
// auditingService
Object result = joinPoint.proceed();
ResponseEntity entity = (ResponseEntity) result;
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
if(!((request.getAttribute(Constants.EVENT_TYPE).toString()).equalsIgnoreCase(AuditEvent.USER_LOGIN.toString()) || (((request.getAttribute(Constants.EVENT_TYPE).toString()).equalsIgnoreCase(AuditEvent.ADMIN_LOGIN.toString()))))){
auditingService.createUserAuditEvent(
(AuditingEvent) request.getAttribute(Constants.EVENT_TYPE),
(EventCreator) request.getAttribute(Constants.EVENT_CREATOR),
(request.getAttribute(Constants.USER_ID)!= null ? request.getAttribute(Constants.USER_ID).toString():""), null,
(request.getAttribute(Constants.NEW_EMAIL_ID) == null ? ""
: request.getAttribute(Constants.NEW_EMAIL_ID).toString()),
(request.getAttribute(Constants.OLD_EMAIL_ID) == null ? ""
: request.getAttribute(Constants.OLD_EMAIL_ID).toString()));
}
return entity;
}
}
From the REST controller the Aspect will be triggered when it finds the corresponding event.
#RestController
public class RegistrationController {
#RequestMapping(path = "/authenticateUser", method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
/* This method call triggers the aspect */
#ResponseBody
public ResponseEntity<String> authenticateUser(HttpServletRequest request, #RequestBody User user)
throws Exception {
request.setAttribute(Constants.EVENT_TYPE, AuditingEvent.USER_LOGIN);
request.setAttribute(Constants.EVENT_CREATOR, EventCreator.USER_FOR_SELF);
request.setAttribute(Constants.USER_ID, user.getUserId());
ResponseEntity<String> responseEntity = null;
try {
// Logic for authentication goes here
responseEntity = new ResponseEntity<>(respData, HttpStatus.OK);
} catch (Exception e) {
request.setAttribute(Constants.EVENT_TYPE, AuditEvent.USER_LOGIN_FAIL);
responseEntity = new ResponseEntity<>(respData, HttpStatus.INTERNAL_SERVER_ERROR);
}
return responseEntity;
}
}
I hope this answer make sense and you can implement similar functionality for Mongo as well.
Cheers !

gson.toJson() throws StackOverflowError and I dont have a circular dependency

I am trying to generate a JSON
Gson gson = new Gson();
String json = gson.toJson(item);
But every time i try to I keep getting a stackoverflow error:
java.lang.StackOverflowError: null
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:239)
at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:968)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:68)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:112)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:239)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:68)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:112)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:239)
at com.google.gson.Gson$FutureTypeAdapter.write(Gson.java:968)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:68)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:112)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:239)
It is caused when i try to convert DataTableResults class which contains a list of Transactions.class as its property field to a string.
The DataTableResult class looks like this :
public class DataTableResults<T> {
/** The draw. */
private String draw;
/** The records filtered. */
private String recordsFiltered;
/** The records total. */
private String recordsTotal;
/** The list of data objects. */
#SerializedName("data")
List<T> listOfDataObjects;
/**
* Gets the draw.
*
* #return the draw
*/
public String getDraw() {
return draw;
}
/**
* Sets the draw.
*
* #param draw the draw to set
*/
public void setDraw(String draw) {
this.draw = draw;
}
/**
* Gets the records filtered.
*
* #return the recordsFiltered
*/
public String getRecordsFiltered() {
return recordsFiltered;
}
/**
* Sets the records filtered.
*
* #param recordsFiltered the recordsFiltered to set
*/
public void setRecordsFiltered(String recordsFiltered) {
this.recordsFiltered = recordsFiltered;
}
/**
* Gets the records total.
*
* #return the recordsTotal
*/
public String getRecordsTotal() {
return recordsTotal;
}
/**
* Sets the records total.
*
* #param recordsTotal the recordsTotal to set
*/
public void setRecordsTotal(String recordsTotal) {
this.recordsTotal = recordsTotal;
}
/**
* Gets the list of data objects.
*
* #return the listOfDataObjects
*/
public List<T> getListOfDataObjects() {
return listOfDataObjects;
}
/**
* Sets the list of data objects.
*
* #param listOfDataObjects the listOfDataObjects to set
*/
public void setListOfDataObjects(List<T> listOfDataObjects) {
this.listOfDataObjects = listOfDataObjects;
}
}
while the Transactions.class looks like this
#Entity
#Table(name = "TRANS")
public class Transactions extends DefaultEntity {
public enum TRANSACTIONTYPE{WITHDRAW, DEPOSIT, WIN, LOSS}
#ManyToOne(targetEntity = User.class)
#JoinColumn(name="player")
private User player;
private double amount;
#Enumerated(EnumType.STRING)
private TRANSACTIONTYPE transactiontype;
private String referenceID;
private String detail;
private String token;
public TRANSACTIONTYPE getTransactiontype() {
return transactiontype;
}
public Transactions setTransactiontype(TRANSACTIONTYPE transactiontype) {
this.transactiontype = transactiontype;
return this;
}
public double getAmount() {
return amount;
}
public Transactions setAmount(double amount) {
this.amount = amount;
return this;
}
public String getReferenceID() {
return referenceID;
}
public Transactions setReferenceID(String referenceID) {
this.referenceID = referenceID;
return this;
}
public String getDetail() {
return detail;
}
public Transactions setDetail(String detail) {
this.detail = detail;
return this;
}
public String getToken() {
return token;
}
public Transactions setToken(String token) {
this.token = token;
return this;
}
public User getPlayer() {
return player;
}
public Transactions setPlayer(User player) {
this.player = player;
return this;
}
}
according to this post, it is supposed to be caused by a circular dependency, but in my case it is not cause i dont have one. What else could cause such error ?
You should remove extends DefaultEntity, you don't need it and may bring circular dependency.
Also you have a #ManyToOne relationship with User, that may cause circular dependency if User also have a reference to Transactions.
If you have it in both parts, you should exclude it from serialization with transient on one part at least.

Spring cache binded only to the current transaction

I'm trying to convince my company to work with spring 3.2 's cache (I know it's very old).
The application is build on top alfresco 5.x (which is build on top of spring 3.2).
Currently, we have some cache binded to the current transaction :
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY) {
cache = (Map<String, Boolean>) AlfrescoTransactionSupport.getResource(CACHED_NAME);
if (cache == null) {
cache = new HashMap<String, Boolean>();
}
AlfrescoTransactionSupport.bindResource(CACHED_NAME, cache);
}
The cache live only for the current read transaction and then, destroyed.
I've tryied
#Cacheable("cache_name")
#Transactional(readOnly=true)
Annotation, but when a read-write transaction is open, the cache is not destroyed.
Any idea how to do that in spring way ?
#biiyamn was right,
I had to implement my own cache to do that.
First of all, i had ti implement the BeanFactory :
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.StringUtils;
public class KReadTransactionCacheFactoryBean implements FactoryBean<KReadTransactionCache>, BeanNameAware,
InitializingBean {
private String name = "";
private boolean allowNullValues = true;
private KReadTransactionCache cache;
/**
* Specify the name of the cache.
* <p>Default is "" (empty String).
*/
public void setName(String name) {
this.name = name;
}
/**
* Set whether to allow {#code null} values
* (adapting them to an internal null holder value).
* <p>Default is "true".
*/
public void setAllowNullValues(boolean allowNullValues) {
this.allowNullValues = allowNullValues;
}
public void setBeanName(String beanName) {
if (!StringUtils.hasLength(this.name)) {
setName(beanName);
}
}
public void afterPropertiesSet() {
this.cache = new KReadTransactionCache(this.name, this.allowNullValues);
}
public KReadTransactionCache getObject() {
return this.cache;
}
public Class<?> getObjectType() {
return KReadTransactionCache.class;
}
public boolean isSingleton() {
return false;
}
}
Then, implement de cache binded to current transaction
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
public class KReadTransactionCache implements Cache {
private static final Object NULL_HOLDER = new NullHolder();
private final String name;
private final boolean allowNullValues;
/**
* Create a new ConcurrentMapCache with the specified name.
* #param name the name of the cache
*/
public KReadTransactionCache(String name) {
this(name, true);
}
protected static Map<Object, Object> getBindedCache(String name) {
Map<Object, Object> cache = null;
if (AlfrescoTransactionSupport.getTransactionReadState() == TxnReadState.TXN_READ_ONLY) {
cache = AlfrescoTransactionSupport.getResource(name);
if (cache == null) {
cache = new HashMap<>();
}
AlfrescoTransactionSupport.bindResource(name, cache);
}
return cache;
}
/**
* Create a new Map with the specified name and the
* given internal ConcurrentMap to use.
* #param name the name of the cache
* #param allowNullValues whether to allow {#code null} values
* (adapting them to an internal null holder value)
*/
public KReadTransactionCache(String name, boolean allowNullValues) {
this.name = name;
this.allowNullValues = allowNullValues;
}
public String getName() {
return this.name;
}
public Map getNativeCache() {
return getBindedCache(name);
}
public boolean isAllowNullValues() {
return this.allowNullValues;
}
public ValueWrapper get(Object key) {
final Map<Object, Object> bindedCache = getBindedCache(name);
if (bindedCache == null) {
return null;
}
Object value = bindedCache.get(key);
return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null);
}
public void put(Object key, Object value) {
final Map<Object, Object> bindedCache = getBindedCache(name);
if (bindedCache == null) {
return;
}
bindedCache.put(key, toStoreValue(value));
}
public void evict(Object key) {
final Map<Object, Object> bindedCache = getBindedCache(name);
if (bindedCache == null) {
return;
}
bindedCache.remove(key);
}
public void clear() {
final Map<Object, Object> bindedCache = getBindedCache(name);
if (bindedCache == null) {
return;
}
bindedCache.clear();
}
/**
* Convert the given value from the internal store to a user value
* returned from the get method (adapting {#code null}).
* #param storeValue the store value
* #return the value to return to the user
*/
protected Object fromStoreValue(Object storeValue) {
if (this.allowNullValues && storeValue == NULL_HOLDER) {
return null;
}
return storeValue;
}
/**
* Convert the given user value, as passed into the put method,
* to a value in the internal store (adapting {#code null}).
* #param userValue the given user value
* #return the value to store
*/
protected Object toStoreValue(Object userValue) {
if (this.allowNullValues && userValue == null) {
return NULL_HOLDER;
}
return userValue;
}
#SuppressWarnings("serial")
private static class NullHolder implements Serializable {
}
}
And the xml configuration :
<!-- *******************************
***** CACHE CONFIGURATION *****
******************************* -->
<!-- simple cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default" />
<bean class="path.to.package.KReadTransactionCacheFactoryBean" p:name="cacheNameByAnnotation" />
<!-- TODO Add other cache instances in here -->
</set>
</property>
</bean>
SimpleCacheManager is useful for testing env as written is spring doc
SimpleCacheManager only supports static mode in which caches is predefined at config time and you are not allowed to add cache at runtime
EhCache and its associated bridge for spring EhCacheManager could be a good choice

Calling stored function in hibernate

I have tried multiple ways to call stored function in hibernate---
1) Via Session.doWork callback method
session.doWork(new Work() {
#Override
public void execute(Connection conn)
throws SQLException {
CallableStatement stmt = conn.prepareCall("{? = call test(?)}");
stmt.registerOutParameter(1, Types.INTEGER);
stmt.setString(2, "callIndex");
stmt.execute();
eventVal = stmt.getInt(1);
}
});
This works fine but cannot return any thing from here, so doesn't solve my purpose.
2) Via Native query
StringBuilder query = new StringBuilder();
query.append("Select test() from dual");
SQLQuery sqlQuery = session.createSQLQuery(query.toString());
List resultList = sqlQuery.list();
events = new ArrayList<Events>();
Object result = null;
if (events != null && !events.isEmpty()) {
for (int i = 0; i < events.size(); i++) {
result = resultList.get(i);
}
}
This doesn't work and gives me some No Dialect mapping for JDBC type -10, not sure of the reason.
3) Via named query
My entity class is ---
#NamedNativeQueries({
#NamedNativeQuery(name = "testCall",
query = "? = call test()",
hints = {#QueryHint(name = "org.hibernate.callable", value = "true" )},
resultClass = Events.class
)
})
#Entity
#Table(name = "EVENTS")
public class Events implements Serializable {
private static final long serialVersionUID = -24850323296832289L;
/** The id. */
#Id
#Column(name = "EVENT_SID")
private Integer eventId;
/** The processed status. */
#Column(name = "EVENT_STATUS")
private String eventStatus;
/** The event type. */
#Column(name = "EVENT_TYPE_NAME")
private String eventType;
/** The live event. */
#Column(name = "IS_LIVE_EVENT")
private Integer liveEvent;
/** The event message. */
#Lob
#Column(name = "EVENT_MESSAGE")
private String eventMessage;
public Integer getEventId() {
return eventId;
}
public void setEventId(Integer eventId) {
this.eventId = eventId;
}
public String getEventStatus() {
return eventStatus;
}
public void setEventStatus(String eventStatus) {
this.eventStatus = eventStatus;
}
/**
* Gets the event type.
*
* #return the event type
*/
public String getEventType() {
return eventType;
}
/**
* Sets the event type.
*
* #param eventType
* the new event type
*/
public void setEventType(String eventType) {
this.eventType = eventType;
}
/**
* Gets the live event.
*
* #return the live event
*/
public Integer getLiveEvent() {
return liveEvent;
}
/**
* Sets the live event.
*
* #param liveEvent
* the new live event
*/
public void setLiveEvent(Integer liveEvent) {
this.liveEvent = liveEvent;
}
/**
* Gets the event message.
*
* #return the event message
*/
public String getEventMessage() {
return eventMessage;
}
/**
* Sets the event message.
*
* #param eventMessage
* the new event message
*/
public void setEventMessage(String eventMessage) {
this.eventMessage = eventMessage;
}
}
The DAO through which I am making call have method ---
public Integer callSqlBlock(){
int event = 0;
Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
List<Events> events = null;
events = session.getNamedQuery("testCall").list();
return events.get(0).getEventId();
}
stored function on the DB end ---
create or replace
function test return SYS_REFCURSOR
as
p_order_recordset SYS_REFCURSOR;
begin
open p_order_recordset FOR SELECT EVENT_SID,EVENT_STATUS,EVENT_TYPE_NAME,IS_LIVE_EVENT FROM events;
return p_order_recordset;
end ;
execution of stored function via named query is giving me invalid column index
Please let me know where I am going wrong on this, and if possible please provide some example also

Spring change date input format

I am trying to create a form, that will send back an object with a timestamp. Right now, the input format must be yyyy-MM-dd HH:mm:ss, I want the timestamp to be entered in the format dd.MM.yyyy HH:mm - how can I change the input format?
The object class:
public class Test {
private Timestamp dateStart;
public Timestamp getDateStart() {
return dateStart;
}
public void setDateStart(Timestamp dateStart) {
this.dateStart = new Timestamp(dateStart.getTime());
}
}
The controller method:
#RequestMapping(value="test", method = RequestMethod.POST)
public View newTest(#ModelAttribute("test") Test test, Model model) {
//save the Test object
}
The jsp form:
<form:form action="service/test" method="post" modelAttribute="test">
<form:input type="text" path="dateStart" />
</form:form>
I get this error, when the format isn't right:
Field error in object 'test' on field 'dateStart': rejected value [22.05.2012 14:00]; codes [typeMismatch.test.dateStart,typeMismatch.dateStart,typeMismatch.java.sql.Timestamp,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [test.dateStart,dateStart]; arguments []; default message [dateStart]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.sql.Timestamp' for property 'dateStart'; nested exception is org.springframework.core.convert.ConversionFailedException: Unable to convert value "22.05.2012 14:00" from type 'java.lang.String' to type 'java.sql.Timestamp'; nested exception is java.lang.IllegalArgumentException: Timestamp format must be yyyy-mm-dd hh:mm:ss[.fffffffff]]
Thanks to Tomasz I got the answer, I have to add a binder method to the controller:
#InitBinder
public void binder(WebDataBinder binder) {binder.registerCustomEditor(Timestamp.class,
new PropertyEditorSupport() {
public void setAsText(String value) {
try {
Date parsedDate = new SimpleDateFormat("dd.MM.yyyy HH:mm").parse(value);
setValue(new Timestamp(parsedDate.getTime()));
} catch (ParseException e) {
setValue(null);
}
}
});
}
FYI, here is the code for a complete Timestamp custom editor (it supports getAsText() too), courtesy of http://adfinmunich.blogspot.com/2011/04/how-to-write-sqltimestamppropertyeditor.html, just change DEFAULT_BATCH_PATTERN to match your desired date/timestamp pattern, OR send the desired pattern when you construct the SqlTimestampPropertyEditor:
package org.springframework.beans.custompropertyeditors;
import java.beans.PropertyEditorSupport;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* Property editor for java.sql.Timestamp, supporting SimpleDateFormat.
*
* Using default Constructor uses the pattern yyyy-MM-dd
* Using the constructor with String, you can use your own pattern.
*
*/
public class SqlTimestampPropertyEditor extends PropertyEditorSupport {
public static final String DEFAULT_BATCH_PATTERN = "yyyy-MM-dd";
private final SimpleDateFormat sdf;
/**
* uses default pattern yyyy-MM-dd for date parsing.
*/
public SqlTimestampPropertyEditor() {
this.sdf = new SimpleDateFormat(SqlTimestampPropertyEditor.DEFAULT_BATCH_PATTERN);
}
/**
* Uses the given pattern for dateparsing, see {#link SimpleDateFormat} for allowed patterns.
*
* #param pattern
* the pattern describing the date and time format
* #see SimpleDateFormat#SimpleDateFormat(String)
*/
public SqlTimestampPropertyEditor(String pattern) {
this.sdf = new SimpleDateFormat(pattern);
}
/**
* #see java.beans.PropertyEditorSupport#setAsText(java.lang.String)
*/
#Override
public void setAsText(String text) throws IllegalArgumentException {
try {
setValue(new Timestamp(this.sdf.parse(text).getTime()));
} catch (ParseException ex) {
throw new IllegalArgumentException("Could not parse date: " + ex.getMessage(), ex);
}
}
/**
* Format the Timestamp as String, using the specified DateFormat.
*/
#Override
public String getAsText() {
Timestamp value = (Timestamp) getValue();
return (value != null ? this.sdf.format(value) : "");
}
}
To use this class, you would define the following #InitBinder:
#InitBinder
public void binder(WebDataBinder binder) {binder.registerCustomEditor(Timestamp.class,
new org.springframework.beans.custompropertyeditors.SqlTimestampPropertyEditor();}
If you want to use a non-default date/timestamp pattern, supply it in the constructor to the SqlTimestampPropertyEditor, so for this particular example you could use:
#InitBinder
public void binder(WebDataBinder binder) {binder.registerCustomEditor(Timestamp.class,
new org.springframework.beans.custompropertyeditors.SqlTimestampPropertyEditor("dd.MM.yyyy HH:mm");}

Resources