I am using Spring 3.2 MVC Controller and a Spring-WS to create a RESTful web-service. The Spring controller accepts an object files an update to the database correctly and then returns JSON to the front-end. The Spring Context is set for message converts for JSON. I have Unit Tests for these, so I know the Spring Controllers are working and are filing data accordingly.
The error, actually a warning, comes when I get the data/JSON back from the web-service:
10:05:08.906[ERROR[Phonebook]10:05:08.902:XRP3:WARN:RestDataSource:restUserDS:restUserDS.userBirthDate:value:-99187200000 failed on validator {type:"isDate",typeCastValidator:true,_generated:true,defaultErrorMessage:"Must be a date."}
com.smartgwt.client.core.JsObject$SGWT_WARN: 10:05:08.902:XRP3:WARN:RestDataSource:restUserDS:restUserDS.userBirthDate: value: -99187200000 failed on validator: {type: "isDate",typeCastValidator: true,_generated: true,defaultErrorMessage: "Must be a date."}
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.google.gwt.dev.shell.MethodAdaptor.invoke(MethodAdaptor.java:105)
at com.google.gwt.dev.shell.MethodDispatch.invoke(MethodDispatch.java:71)
at com.google.gwt.dev.shell.OophmSessionHandler.invoke(OophmSessionHandler.java:172)
at com.google.gwt.dev.shell.BrowserChannelServer.reactToMessages(BrowserChannelServer.java:293)
at com.google.gwt.dev.shell.BrowserChannelServer.processConnection(BrowserChannelServer.java:547)
at com.google.gwt.dev.shell.BrowserChannelServer.run(BrowserChannelServer.java:364)
at java.lang.Thread.run(Thread.java:662)
So, here is my UserDataSource:
package com.opensource.restful.client.datasource;
import java.util.HashMap;
import java.util.Map;
import com.google.gwt.core.client.JavaScriptObject;
import com.opensource.restful.shared.Constants;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DSResponse;
import com.smartgwt.client.data.OperationBinding;
import com.smartgwt.client.data.RestDataSource;
import com.smartgwt.client.data.fields.DataSourceBooleanField;
import com.smartgwt.client.data.fields.DataSourceDateField;
import com.smartgwt.client.data.fields.DataSourceIntegerField;
import com.smartgwt.client.data.fields.DataSourceTextField;
import com.smartgwt.client.types.DSDataFormat;
import com.smartgwt.client.types.DSOperationType;
import com.smartgwt.client.types.DSProtocol;
import com.smartgwt.client.util.JSOHelper;
import com.smartgwt.client.util.JSON;
public class UserDataSource extends RestDataSource
{
private static UserDataSource instance = null;
public static UserDataSource getInstance()
{
if (instance == null)
{
instance = new UserDataSource("restUserDS");
}
return instance;
}
private UserDataSource(String id)
{
setID(id);
setClientOnly(false);
// set up FETCH to use GET requests
OperationBinding fetch = new OperationBinding();
fetch.setOperationType(DSOperationType.FETCH);
fetch.setDataProtocol(DSProtocol.GETPARAMS);
DSRequest fetchProps = new DSRequest();
fetchProps.setHttpMethod("GET");
fetch.setRequestProperties(fetchProps);
// set up ADD to use POST requests
OperationBinding add = new OperationBinding();
add.setOperationType(DSOperationType.ADD);
add.setDataProtocol(DSProtocol.POSTMESSAGE);
// ===========================================
DSRequest addProps = new DSRequest();
addProps.setHttpMethod("POST");
// addProps.setContentType("application/json");
add.setRequestProperties(addProps);
// set up UPDATE to use PUT
OperationBinding update = new OperationBinding();
update.setOperationType(DSOperationType.UPDATE);
update.setDataProtocol(DSProtocol.POSTMESSAGE);
// ===========================================
DSRequest updateProps = new DSRequest();
updateProps.setHttpMethod("PUT");
// updateProps.setContentType("application/json");
update.setRequestProperties(updateProps);
// set up REMOVE to use DELETE
OperationBinding remove = new OperationBinding();
remove.setOperationType(DSOperationType.REMOVE);
DSRequest removeProps = new DSRequest();
removeProps.setHttpMethod("DELETE");
remove.setRequestProperties(removeProps);
// apply all the operational bindings
setOperationBindings(fetch, add, update, remove);
init();
}
private DataSourceIntegerField userIdField;
private DataSourceBooleanField userActiveField;
private DataSourceTextField usernameField;
private DataSourceTextField passwordField;
private DataSourceTextField firstnameField;
private DataSourceTextField lastnameField;
private DataSourceTextField emailField;
private DataSourceTextField securityQuestion1Field;
private DataSourceTextField securityAnswer1Field;
private DataSourceTextField securityQuestion2Field;
private DataSourceTextField securityAnswer2Field;
private DataSourceDateField birthdateField;
private DataSourceIntegerField positionIdField;
protected void init()
{
setDataFormat(DSDataFormat.JSON);
setJsonRecordXPath("/");
// set the values for the datasource
userIdField = new DataSourceIntegerField(Constants.USER_ID, Constants.TITLE_USER_ID);
userIdField.setPrimaryKey(true);
userIdField.setCanEdit(false);
userActiveField = new DataSourceBooleanField(Constants.USER_ACTIVE, Constants.TITLE_USER_ACTIVE);
usernameField = new DataSourceTextField(Constants.USER_USERNAME, Constants.TITLE_USER_USERNAME);
passwordField = new DataSourceTextField(Constants.USER_PASSWORD, Constants.TITLE_USER_PASSWORD);
firstnameField = new DataSourceTextField(Constants.USER_FIRST_NAME, Constants.TITLE_USER_FIRST_NAME);
lastnameField = new DataSourceTextField(Constants.USER_LAST_NAME, Constants.TITLE_USER_LAST_NAME);
emailField = new DataSourceTextField(Constants.USER_EMAIL, Constants.TITLE_USER_EMAIL);
securityQuestion1Field =
new DataSourceTextField(Constants.USER_SECURITY_QUESTION_1, Constants.TITLE_USER_SECURITY_QUESTION_1);
securityAnswer1Field =
new DataSourceTextField(Constants.USER_SECURITY_ANSWER_1, Constants.TITLE_USER_SECURITY_ANSWER_1);
securityQuestion2Field =
new DataSourceTextField(Constants.USER_SECURITY_QUESTION_2, Constants.TITLE_USER_SECURITY_QUESTION_2);
securityAnswer2Field =
new DataSourceTextField(Constants.USER_SECURITY_ANSWER_2, Constants.TITLE_USER_SECURITY_ANSWER_2);
birthdateField = new DataSourceDateField(Constants.USER_BIRTHDATE, Constants.TITLE_USER_BIRTHDATE);
positionIdField = new DataSourceIntegerField(Constants.USER_POSITION_ID, Constants.TITLE_USER_POSITION_ID);
// positionActiveField = new DataSourceBooleanField(Constants.USER_ACTIVE, Constants.TITLE_USER_ACTIVE);
// positionCodeField;
// positionDescriptionField;
setFields(userIdField, userActiveField, usernameField, passwordField, firstnameField, lastnameField,
emailField, birthdateField, securityQuestion1Field, securityAnswer1Field, securityQuestion2Field,
securityAnswer2Field, positionIdField);
setFetchDataURL(getServiceRoot() + "/userId/{id}"); // works great
setAddDataURL(getServiceRoot() + "/create");
setUpdateDataURL(getServiceRoot() + "/update");
setRemoveDataURL(getServiceRoot() + "/remove"); // works great
}
protected String getServiceRoot()
{
return "rest/users";
}
protected String getPrimaryKeyProperty()
{
return "userId";
}
#Override
protected Object transformRequest(DSRequest dsRequest)
{
System.out.println("UserDataSource: transformRequest: START");
dsRequest.setContentType("application/json");
JavaScriptObject jso = dsRequest.getData();
String jsoText = JSON.encode(jso);
System.out.println("UserDataSource: transformRequest: START: jsoText=" + jsoText);
// ================================================================================
// String strDob = JSOHelper.getAttribute(jso, Constants.USER_BIRTHDATE);
// Date dateDob = JSOHelper.getAttributeAsDate(jso, Constants.USER_BIRTHDATE);
// JSOHelper.setAttribute(jso, Constants.USER_BIRTHDATE, dateDob.getTime());
// System.out.println("UserDataSource: transformRequest: START2: jsoText2=" + jsoText);
// ================================================================================
// get the user position id which comes from the UI
// the name of this field from the UI 'userPositionId'
String userPositionId = JSOHelper.getAttribute(jso, Constants.USER_POSITION_ID);
// create a small JavaScriptObject to be used for the position
// the JSON string would look like {"id":x} x = userPositionId
Map mapPositionId = new HashMap();
mapPositionId.put("id", userPositionId);
JavaScriptObject jsoPositionId = JSOHelper.convertMapToJavascriptObject(mapPositionId);
// This creates the new JSON attribute:
// ... , "position":{"id":x}
JSOHelper.setAttribute(jso, "position", jsoPositionId);
// remove the JSON Attribute: ... , "userPositionId":x
JSOHelper.deleteAttribute(jso, Constants.USER_POSITION_ID);
String s1 = JSON.encode(jso);
System.out.println("UserDataSource: transformRequest: FINISH: s1=" + s1);
return s1;
// return super.transformRequest(dsRequest);
}
protected void transformResponse(DSResponse response, DSRequest request, Object data)
{
System.out.println("UserDataSource: transformResponse: START");
super.transformResponse(response, request, data);
System.out.println("UserDataSource: transformResponse: FINISH");
}
}
I can confirm I am sending data/JSON just fine. I have to make a slight change to add an attribute that I am sending back. And I believe that is the purpose of TransformRequest.
The Spring MVC Controller receiving the Update looks like:
#RequestMapping(value="/update",
method=RequestMethod.PUT,produces="application/json",
headers="content-type=application/json")
public #ResponseBody UserDTO updateUser(#RequestBody UserDTO user)
{
System.out.println("UserController: START: updateUser: user=" + user);
UserEntity userEntity = service.update(user);
UserDTO userDto = Mapping.mappingUser(userEntity);
System.out.println("UserController: FINISH: updateUser: userDto=" + userDto);
return userDto;
}
And I can confirm I am getting a valid UserDTO. When I look at the transformResponse:
System.out.println("UserDataSource: transformResponse: START");
super.transformResponse(response, request, data);
System.out.println("UserDataSource: transformResponse: FINISH");
I get the error on the first println, I haven't even done the super.transformResponse just yet. When I look at the data coming back, this is the JSON I am getting back.
{
"userId":1,
"userActive":true,
"position":{
"id":1,
"active":true,
"code":"ADMIN",
"description":"Administrator"
},
"username":"demo",
"password":"demo",
"otherPassword":null,
"userFirstName":"DemoXXX",
"userLastName":"DemoXXX",
"userEmail":"tom#tomholmes.netXXX",
"userSecurityQuestion1":"Meaning of Life?XXX",
"userSecurityAnswer1":"42XX",
"userSecurityQuestion2":"aaaXX",
"userSecurityAnswer2":"bbbXX",
"userBirthDate":-99100800000,
"contacts":[
{
"contactId":2,
"userId":1,
"prefix":"Mr.",
"firstName":"updated_fn",
"middleName":null,
"lastName":"updated_ln",
"suffix":"Jr.",
"address1":"123 main street",
"address2":"Apt. 456",
"city":"Randolph",
"state":"MA",
"zip":"12345-1234",
"companyId":0,
"enteredBy":0,
"enteredDate":null,
"editedBy":0,
"editedDate":null,
"birthDate":null,
"emails":null,
"phones":null,
"links":null
}
],
"userPositionId":null
}
So ... How do I fix my datasource or transformResponse to remove this warning? The JSON appears to be correct, and the only issue is with the "userBirthDate" when it comes back as a long negative number, I presume the milliseconds from the epoch. Is there some change I can make in the JSON/Jackson Mapper to change how the dates are formatted?
Thanks for any help!
UPDATE 1:
The help provided below was helpful, and now I know this is not a SmartGWT or RestDataSource issue and is strictly with how jackson converts a java.util.Date within an object. The conversion changes dates to a negative long number and should have another format. I am using Spring 3.2 and was using the old Jackson 1.9.14. But now, I upgraded to Jackson 2, and my pom.xml now uses:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.1.4</version>
</dependency>
Within my spring-servlext.xml:
<context:component-scan base-package="com.opensource.restful" />
<bean id="jsonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes" value="application/json"/>
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg type="java.lang.String" value="yyyy-MM-dd'T'HH:mm:ssZ"></constructor-arg>
</bean>
</property>
</bean>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jsonHttpMessageConverter"/>
</list>
</property>
</bean>
<mvc:annotation-driven />
I have been Googling for a few hours now and looking for a solution that uses the Jackson2 mapper within the Spring Configuration, and after I make sure I get all the bean definitions correct, the userBirthDate is still coming back as a negative long. I am sure this configuration can be tweaked just a bit to get it the way I want, so the date comes back as the ISO format: yyyy-MM-dd'T'HH:mm:ssZ
Thanks for helping me get closer.
UPDATE 2:
I think I did it. As previously stated, I upgraded to Jackson2 which I understand is already part of Spring 3.2, which is the version of Spring I am using.
The spring-servlet.xml that I am using, and which does work looks like:
<context:component-scan base-package="com.opensource.restful" />
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg type="java.lang.String" value="yyyy-MM-dd'T'HH:mm:ssZ"></constructor-arg>
</bean>
</property>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="jsonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes" value="application/json"/>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jsonHttpMessageConverter" />
</list>
</property>
</bean>
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
<property name="messageConverters">
<list>
<ref bean="jsonHttpMessageConverter" />
</list>
</property>
</bean>
I had to add MappingJackson2HttpMessageConverter the second time because, it's referenced in the restTemplate ... but If I could just define it once, that would be fine. So, maybe someone can help me define the spring-servlet.xml better.
Anyway, this change works and as a result the JSON date comes back as:
"userBirthDate":"1966-11-03T00:00:00-0500"
so, that's progress so far.
From the validation error - defaultErrorMessage:"Must be a date"
Since birthdateField is DataSourceDateField, your UserDTO.userBirthDate must be a java.util.Date or similar and have Date getUserBirthDate().
And Constants.USER_BIRTHDATE must be set to "userBirthDate".
If all above is alright, its due to default serialization of java.util.Date object to JSON.
Check following for additional information on that.
http://java.dzone.com/articles/how-serialize-javautildate (Do not use static SimpleDateFormat)
Spring 3.1 JSON date format
jackson2 JSON ISO 8601 date from JodaTime in Spring 3.2RC1
SmartGWT works best when following date format is used (e.g.- 2013-05-09T00:00:00).
yyyy-MM-dd'T'HH:mm:ss
System.out.println() can not be used in SmartGWT/GWT as client side code is converted to JavaScript and run inside the browser, without a JVM.
You probably won't need to use transformResponse() in this case.
Related
I am using Jackson 2.x and spring-ws 4.0.6.
I have configured a rest service using CXF and configured mapper.
<jaxrs:providers>
<ref bean="jsonReader"/>
</jaxrs:providers>
Then configured this mapper like this
<bean id="jsonBodyReader" class="com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider">
<property name="mapper" ref="mapper"/>
</bean>
<bean id="mapper"
class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="annotationIntrospector">
<bean class="com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector"/>
</property>
<property name="featuresToEnable">
<array>
<util:constant static-field="com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_WITH_ZONE_ID"/>
<util:constant static-field="com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES"/>
</array>
</property>
<property name="featuresToDisable">
<array>
<util:constant static-field="com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS"/>
<util:constant static-field="com.fasterxml.jackson.databind.SerializationFeature.WRITE_EMPTY_JSON_ARRAYS"/>
<util:constant static-field="com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES"/>
</array>
</property>
<property name="serializationInclusion" value="NON_NULL"/>
</bean>
Now I want to map this json to List<HashMap<String,String>>
{
"identities": [{
"id1": "12345",
"id2": "16777"
},{
"id3": "12345"
}]
}
I can get as many ids - id1, id2, id3.. so I cannot create a pojo with fields name id1, id2 and so on.
I annotated the field in my POJO like this
#JsonDeserialize(using = CustDeserializer.class)
private List<HashMap<String,String>> identities;
CustDeserializer looks like this
public class CustDeserializer extends JsonDeserializer>> {
#Override
public List> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
List> identities = new ArrayList>();
for(int i=0;i> elem = node1.fields();
HashMap map= new HashMap();
while(elem.hasNext()){
Entry el = elem.next();
String name = el.getKey();
String value = el.getValue().asText();
map.put(name, value);
}
identities.add(map);
}
return identities;
}
Now when I call the rest service, the list is populated with hashmaps containing key/value pairs. But CustDeserializer class is not invoked so I am not able to customize my list. It is getting transformed by Jackson's CollectionDeserializer by default.
When I try the same in a standalone java program, it works and my deserializer is called
ObjectMapper mapper = new ObjectMapper();
try {
JsonRequest h = mapper.readValue(new File("request.json"), JsonRequest.class);
System.out.println(h);
}
Output: JsonRequest [name=XYZ, identities=[{id2=16777, id1=12345}, {id3=12345}]]
But somehow it doesn't work when I am using it in the context of CXF and Spring.
I am building a simple Spring MVC webapp and was developing on jetty. My controller binding used this:
#RequestMapping(value = RESTRoutes.CREATE_DOC, method = RequestMethod.POST)
public #ResponseBody String getDoc
And returning a String from a JSONObject correctly resolves to JSON in my ajax response.
But using those same controllers, i deployed my gradle war to tomcat and my json came back wrapped as true strings.
So i changed my headers to use Map and that seems to fix things in both jetty and tomcat:
#RequestMapping(value = RESTRoutes.CREATE_DOC, method = RequestMethod.POST)
public #ResponseBody Map<String, String> getDoc
I convert from the string to a map with this:
HashMap<String, String> jsonResponse = new HashMap<String, String>();
if(claimFolder.has("error")){
response.setStatus(500);
}else{
jsonResponse = new ObjectMapper().readValue(claimFolder.toString(), HashMap.class);
}
return jsonResponse;
My question is why this is nessesary?
Here's my jackson converter configuration:
<bean id="formConverter" class="org.springframework.http.converter.FormHttpMessageConverter" />
<!-- add byte[] converter -->
<bean id="byteArrayConverter" class="org.springframework.http.converter.ByteArrayHttpMessageConverter">
<property name="supportedMediaTypes" value="application/octet-stream" />
</bean>
<!-- add in our JSON message converter -->
<bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="supportedMediaTypes" value="application/json;charset=UTF-8" />
</bean>
<!-- add in our plain string message converter -->
<bean id="stringHttpMessageConverter" class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes" value="text/plain;charset=UTF-8" />
</bean>
<!-- Expose the authenticated handler to all beans that have been declared via annotation -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
</bean>
TL;DR: Why does jetty and tomcat return stringified JSON differently?
Well, it's absolutely normal for Spring content negotiation to translate a String object as a simple string without marshalling it to a JSON object. In order to serialize a java String object in JSON object you need to wrap it previously in some java class. For example:
QuestionStatus {
private String status;
public QuestionStatus(String status) {
this.status = status;
}
public getStatus() {
return status;
}
}
Hence you have to return in your Controller method not a String but QuestionStatus.
I'm using Spring MVC 3 and MappingJacksonHttpMessageConverter in order to get the json data with #ResponseBody. With the default config works ok but now i need to transform the camelCase fields to Pascal casing. For this purpose, i've developed a custom naming strategy:
UpperCaseNamingStrategy.java
public class UpperCaseNamingStrategy extends PropertyNamingStrategy {
#Override
public String nameForField(MapperConfig config, AnnotatedField field, String defaultName){
return convert(defaultName);
}
#Override
public String nameForGetterMethod(MapperConfig config, AnnotatedMethod method, String defaultName){
return convert(defaultName);
}
#Override
public String nameForSetterMethod(MapperConfig config, AnnotatedMethod method, String defaultName){
return convert(defaultName);
}
public String convert(String defaultName){
char[] arr= defaultName.toCharArray();
if(arr.length != 0){
if(Character.isLowerCase(arr[0])){
arr[0] = Character.toUpperCase(arr[0]);
}
}
return new StringBuilder().append(arr).toString();
}
}
I set my custom strategy to the objectMapper and i set the objectMapper in the converter. These are the beans:
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="objectMapper" ref="jacksonObjectMapper" />
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jacksonMessageConverter"/>
</list>
</property>
</bean>
<bean id="jacksonObjectMapper" class="org.codehaus.jackson.map.ObjectMapper">
<property name="propertyNamingStrategy" ref="namingStrategy"/>
</bean>
<bean id="namingStrategy" class="es.unican.meteo.util.UpperCaseNamingStrategy"></bean>
The beans are registered properly because i can see it in the log but when i request the json data the behaviour is the same and the converter method is not called. Do I need more configs?
Following changes are suggested as compared to what I did in my project:
Change mapper bean class to "com.fasterxml.jackson.databind.ObjectMapper". I am using Spring 4.3
add #JsonProperty annotation to the property of class which is being serielized/deseralized
Create default constructors in class which is being serielized/deseralized
Best of Luck!
According to http://wiki.fasterxml.com/JacksonFAQDateHandling, “DateTime can be automatically serialized/deserialized similar to how java.util.Date is handled.” However, I am not able to accomplish this automatic functionality. There are StackOverflow discussions related to this topic yet most involve a code-based solution, but based upon the quote above I should be able to accomplish this via simple configuration.
Per http://wiki.fasterxml.com/JacksonFAQDateHandling I have my configuration set so that writing dates as timestamps is false. The result is that java.util.Date types are serialized to ISO 8601 format, but org.joda.time.DateTime types are serialized to a long object representation.
My environment is this:
Jackson 2.1
Joda time 2.1
Spring 3.2
Java 1.6
My Spring configuration for the jsonMapper bean is
#Bean
public ObjectMapper jsonMapper() {
ObjectMapper objectMapper = new ObjectMapper();
//Fully qualified path shows I am using latest enum
ObjectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.
WRITE_DATES_AS_TIMESTAMPS , false);
return objectMapper;
}
My test code snippet is this
Date d = new Date();
DateTime dt = new DateTime(d); //Joda time
Map<String, Object> link = new LinkedHashMap<String, Object>();
link.put("date", d);
link.put("createdDateTime", dt);
The resulting snippet of JSON output is this:
{"date":"2012-12-24T21:20:47.668+0000"}
{"createdDateTime": {"year":2012,"dayOfMonth":24,"dayOfWeek":1,"era":1,"dayOfYear":359,"centuryOfEra":20,"yearOfEra":2012,"yearOfCentury":12,"weekyear":2012,"monthOfYear":12 *... remainder snipped for brevity*}}
My expectation is that the DateTime object should matche that of the Date object based upon the configuration. What am I doing wrong, or what am I misunderstanding? Am I reading too much into the word automatically from the Jackson documentation and the fact that a string representation was produced, albeit not ISO 8601, is producing the advertised automatic functionality?
I was able to get the answer to this from the Jackson user mailing list, and wanted to share with you since it is a newbie issue. From reading the Jackson Date FAQ, I did not realize that extra dependencies and registration are required, but that is the case. It is documented at the git hub project page here https://github.com/FasterXML/jackson-datatype-joda
Essentially, I had to add another dependency to a Jackson jar specific to the Joda data type, and then I had to register the use of that module on the object mapper. The code snippets are below.
For my Jackson Joda data type Maven dependency setup I used this:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>${jackson.version}</version>
</dependency>
To register the Joda serialization/deserialization feature I used this:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JodaModule());
objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.
WRITE_DATES_AS_TIMESTAMPS , false);
Using Spring Boot.
Add to your Maven configuration...
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.7.5</version>
</dependency>
Then to your WebConfiguration...
#Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter
{
public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
{
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
final ObjectMapper objectMapper = new ObjectMapper();
//configure Joda serialization
objectMapper.registerModule(new JodaModule());
objectMapper.configure(
com.fasterxml.jackson.databind.SerializationFeature.
WRITE_DATES_AS_TIMESTAMPS , false);
// Other options such as how to deal with nulls or identing...
objectMapper.setSerializationInclusion (
JsonInclude.Include.NON_NULL);
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
converter.setObjectMapper(objectMapper);
converters.add(converter);
super.configureMessageConverters(converters);
}
}
In Spring Boot the configuration is even simpler. You just declare Maven dependency
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
and then add configuration parameter to your application.yml/properties file:
spring.jackson.serialization.write-dates-as-timestamps: false
I thought I'd post an updated working example using:
Spring 4.2.0.RELEASE, Jackson 2.6.1, Joda 2.8.2
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
">
<!-- DispatcherServlet Context: defines this servlet's request-processing
infrastructure -->
<!-- Enables the Spring MVC #Controller programming model -->
<annotation-driven>
<message-converters>
<beans:bean
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<beans:property name="objectMapper" ref="objectMapper" />
</beans:bean>
</message-converters>
</annotation-driven>
<!-- Handles HTTP GET requests for /resources/** by efficiently serving
up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by #Controllers to .jsp resources
in the /WEB-INF/views directory -->
<beans:bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<beans:bean id="objectMapper"
class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<beans:property name="featuresToDisable">
<beans:array>
<util:constant
static-field="com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS" />
</beans:array>
</beans:property>
<beans:property name="modulesToInstall"
value="com.fasterxml.jackson.datatype.joda.JodaModule" />
</beans:bean>
<beans:bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<beans:property name="defaultLocale" value="en" />
</beans:bean>
<!-- Configure the Message Locale Resources -->
<beans:bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basename" value="errors" />
</beans:bean>
<beans:bean id="versionSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basename" value="version" />
</beans:bean>
<!-- DataSource -->
<beans:bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<beans:property name="jndiName" value="java:comp/env/jdbc/TestDB" />
</beans:bean>
<!-- POJO: Configure the DAO Implementation -->
<beans:bean id="publicationsDAO"
class="com.test.api.publication.PublicationsDAOJdbcImpl">
<beans:property name="dataSource" ref="dataSource" />
</beans:bean>
<!-- Things to auto-load -->
<context:component-scan base-package="com.test.api" />
<context:component-scan base-package="com.test.rest" />
</beans:beans>
API Code
package com.test.api.publication;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
#JsonRootName("event")
#JsonIgnoreProperties(ignoreUnknown=true)
public class Publication {
private Map<String, Object> tokens;
private String href;
private String policy_path;
#JsonProperty("tokens")
public Map<String, Object> getTokens() {
return tokens;
}
#JsonProperty("tokens")
public void setTokens(Map<String, Object> tokens) {
this.tokens = tokens;
}
#JsonProperty("href")
public String getHref() {
return href;
}
#JsonProperty("href")
public void setHref(String href) {
this.href = href;
}
#JsonProperty("policyPath")
public String getPolicyPath() {
return policy_path;
}
#JsonProperty("policyPath")
public void setPolicyPath(String policy_path) {
this.policy_path = policy_path;
}
}
package com.test.api.publication;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PublicationsDAOJdbcImpl implements PublicationsDAO{
static final Logger logger = LoggerFactory.getLogger(PublicationsDAOJdbcImpl.class.getName());
private DataSource _dataSource;
#Override
public void setDataSource(DataSource ds) {
// TODO Auto-generated method stub
}
#Override
public void close() {
// TODO Auto-generated method stub
}
#Override
public Publication getPublication(String policyPath) {
Publication ret = new Publication();
//TODO: do something
return ret;
}
}
package com.test.rest.publication;
import java.util.HashMap;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.test.api.publication.Publication;
import com.test.api.publication.PublicationsDAO;
import com.test.rest.error.UnknownResourceException;
#RestController
#RequestMapping("/pub")
public class PublicationController {
private static final Logger logger = LoggerFactory.getLogger(PublicationController.class);
#Autowired
#Qualifier("publicationsDAO")
private PublicationsDAO publicationsDAO;
/**********************************************************************************************************************
*
* #param policyPath
* #return
* #throws UnknownResourceException
*/
#RequestMapping(value = "/{policyPath}", method = RequestMethod.GET)
public Publication getByPolicyPath(#PathVariable String policyPath) throws UnknownResourceException{
logger.debug("policyPath=" + policyPath);
Publication ret = publicationsDAO.getPublication(policyPath);
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("TEST1", null);
map.put("TEST2", new Integer(101));
map.put("TEST3", "QuinnZilla");
map.put("TEST4", new DateTime());
ret.setTokens(map);
return ret;
}
}
And I get the output result
{
"tokens": {
"TEST2": 101,
"TEST3": "QuinnZilla",
"TEST4": "2015-10-06T16:59:35.120Z",
"TEST1": null
},
"href": null,
"policyPath": null
}
I'm hoping someone can help me since I have been banging my head against a wall for a couple of days on a issue which seems straightforward and which has been documented in other threads on the web.
I am using Smart GWT client (3.0) in conjunction with Spring 3.1 server and using JSON to communicate (with Jackson API 1.9).
The issue is that when I attempt to save a date from my SmartGWT client and it is sent to the server I get the following exception:
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'comment' on field 'dateAdded': rejected value [2012-06-27T10:57:47+0100]; codes [typeMismatch.comment.dateAdded,typeMismatch.dateAdded,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [comment.dateAdded,dateAdded]; arguments []; default message [dateAdded]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'dateAdded'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.lang.String to type java.util.Date for value '2012-06-27T10:57:47+0100'; nested exception is java.lang.IllegalArgumentException]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:110)
I have seen this issue in a few other posts, but most relate to not having formatted the Date in the correct format, but I have tried various formats:
- yyyy-MM-dd
- yyyy-MM-dd'T'HH:mm:ssZ
- yyyyMMddHHmmssZ (as per suggestion here: http://code.google.com/p/usersapi/issues/detail?id=8)
So in my code I have done the following:
Configured a CustomObjectMapper:
`
public class CustomObjectMapper extends ObjectMapper {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
public CustomObjectMapper() {
super();
configure(Feature.WRITE_DATES_AS_TIMESTAMPS, false);
setDateFormat(formatter);
getDeserializationConfig().setDateFormat(formatter);
}
}
`
Spring app context thusly:
`
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
<constructor-arg ref="jaxbMarshaller" />
<property name="supportedMediaTypes" value="application/xml"/>
</bean>
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="objectMapper" ref="jacksonObjectMapper" />
<property name="supportedMediaTypes" value="application/json" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<context:component-scan base-package="com.jpmorgan.creditriskreporting.server" />
<bean id="marshallingConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
<constructor-arg ref="jaxbMarshaller" />
<property name="supportedMediaTypes" value="application/xml"/>
</bean>
<bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
<property name="supportedMediaTypes" value="application/json" />
<property name="objectMapper" ref="jacksonObjectMapper" />
</bean>
<bean id="jacksonObjectMapper" class="com.jpmorgan.creditriskreporting.server.util.CustomObjectMapper" />
<!-- Client -->
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
<property name="messageConverters">
<list>
<ref bean="marshallingConverter" />
<ref bean="jsonConverter" />
</list>
</property>
</bean>
`
Bean object:
`
import java.util.Date;
#JsonAutoDetect
public class Comment {
private int id;
private String comment;
private Date dateAdded;
public Comment() {}
public Comment(int id) {
this.id = id;
}
...
//#JsonSerialize(using=JsonDateSerializer.class) -- I had previously tried to use these custom Date serializer class
public Date getDateAdded() {
return dateAdded;
}
//#JsonDeserialize(using=JsonDateDeserializer.class)
public void setDateAdded(Date dateAdded) {
this.dateAdded = dateAdded;
}
`
EDIT:
Controller Class
This may be where the issue lies, since when I use #RequestBody it works from my Integration tests, however, my Abstract RestDataSource in SmartGWT only works with #ModelAttribute, so I'm not sure how to proceed.
#RequestMapping(value="/", method=RequestMethod.POST)
public #ResponseBody Comment createNewComment2(#ModelAttribute Comment comment) {
log.info("calling createComment with comment: {}", comment);
comment.setDateAdded(new Date());
Comment added = commentDao.create(comment);
log.info("created comment: {}", added);
return commentDao.get(comment);
}
So I can fetch data from the server and the date is displayed in SmartGWT fine. It's only when I do the add data that I get the issue. From Smart GWT Developer Console:
{
"dataSource":"CommentDS",
"operationType":"add",
"componentId":"isc_DynamicForm_1",
"data":{
"userAdded":"sharper",
"dateAdded":"2012-06-27T10:57:47+0100",
"comment":"sample"
},
"callback":{
"target":[DynamicForm ID:isc_DynamicForm_1],
"methodName":"saveEditorReply"
},
"showPrompt":true,
"prompt":"Saving form...",
"oldValues":{
},
"clientContext":{
},
"requestId":"CommentDS$6272"
}
Any help with this is hugely appreciated.
Cheers,
Steve
I found out the issue thanks to http://vkubushyn.wordpress.com/2011/05/31/smart-gwt-restful-spring-mvc
Had to use Spring's InitBinder
#InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
You should add DateFormat into your model.
#DateTimeFormat(pattern = "dd.MM.yyyy")
private Date beginDate;
#DateTimeFormat(pattern = "dd.MM.yyyy")
private Date endDate;
as a function parameter
void functionName** (#RequestParam("beginDate") #DateTimeFormat(pattern = "dd.MM.yyyy")Date beginDate, #RequestParam("endDate") #DateTimeFormat(pattern = "dd.MM.yyyy")Date endDate)
I might be wrong, but as far as I remember the Z stands for timezone in ISOwhoknowswhatformat. And that's 4 chars wide, so I would try this:
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZ");
By the way: if this is the issue you should've catched it in your unit tests. You do have unit test for CustomObjectMapper don't you? :P