Search code examples
jsonrestdatesmartgwt

smartgwt restdatasource json date validation


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.


Solution

  • 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.