javajqueryjsonstruts2model-driven

net.sf.json.JSONException: There is a cycle in the hierarchy in Struts 2


I am using Struts 2 class which implements ModelDriven. I am able to pass the data from jQuery and save the data on the database.

When I try to retrieve the data back and pass it back to jQuery, I am not sure why it's not available in jQuery. I am sure I am missing something with the basic flow.

Here is my action class:

public HttpHeaders index() {
    model = projectService.getProjectDetails(project.getUserID());
    return new DefaultHttpHeaders("success").setLocationId("");
} 

@Override
public Object getModel() {
    return project;
}

public Project getProject() {
    return project;
}

public void setProject(Project project) {
    this.project = project;
}

Here is my jQuery:

function getProjectDetails() {
    var userID = localStorage.getItem('userID');
    var request = $.ajax({
        url : '/SUH/project.json',
        data : {
            userID : userID
        },
        dataType : 'json',
        type : 'GET',
        async : true
    });

    request.done(function(data) {
        console.log(JSON.stringify(data));
        $.each(data, function(index, element) {
            console.log('element project--->' + index + ":" + element);
            
        });
    });

    request.fail(function(jqXHR, textStatus) {
        console.log('faik');
    });
}

The model object in the Action class has all the data available but I tried to return model or project objects but both didn't work.


Solution

  • By default Struts2 REST plugin is using json-lib for serialization your beans. If you are using ModelDriven then it accesses your model directly when it processes a result. Since you are using the extension .json in the request URL the content type handler is selected by extension. It should be JsonLibHandler.

    This handler is using JSONArray.fromObject(obj) if obj is an array or list or JSONObject.fromObject(obj) otherwise to get JSONObejct that could be serialized and written to the response.

    The obj is the value returned by getModel(), in your case it will be project.

    Because JsonLibHandler is using default JsonConfig you can't exclude properties from the bean to be serialized unless they are public fields.

    The following features of json-lib could be powered with JsonConfig:

    • Cycle detection, there are two default strategies (default throws an exception), you can register your own
    • Skip transient fields when serailizing to JSON (default=don't skip) Skip JAP @Transient annotated methods when serailizing to JSON (default=don't skip)
    • Exclude bean properties and/or map keys when serailizing to JSON (default=['class','metaClass','declaringClass'])
    • Filters provide a finer detail for excluding/including properties when serializing to JSON or transforming back to Java

    You can find this code snippets that allows you to exclude some properties.

    Exclude properties

    String str = "{'string':'JSON', 'integer': 1, 'double': 2.0, 'boolean': true}";  
    JsonConfig jsonConfig = new JsonConfig();  
    jsonConfig.setExcludes( new String[]{ "double", "boolean" } );  
    JSONObject jsonObject = (JSONObject) JSONSerializer.toJSON( str, jsonConfig );  
    assertEquals( "JSON", jsonObject.getString("string") );        
    assertEquals( 1, jsonObject.getInt("integer") );        
    assertFalse( jsonObject.has("double") );     
    assertFalse( jsonObject.has("boolean") );     
    

    Exclude properties (with filters)

    String str = "{'string':'JSON', 'integer': 1, 'double': 2.0, 'boolean': true}";  
    JsonConfig jsonConfig = new JsonConfig();  
    jsonConfig.setJsonPropertyFilter( new PropertyFilter(){    
       public boolean apply( Object source, String name, Object value ) {    
          if( "double".equals(value) || "boolean".equals(value) ){    
             return true;    
          }    
          return false;    
       }    
    });    
    JSONObject jsonObject = (JSONObject) JSONSerializer.toJSON( str, jsonConfig );  
    assertEquals( "JSON", jsonObject.getString("string") );        
    assertEquals( 1, jsonObject.getInt("integer") );        
    assertFalse( jsonObject.has("double") );     
    assertFalse( jsonObject.has("boolean") );
    

    But you have an option to use your own ContentTypeHandler to override defaults.

    The alternative is to use Jackson library to handle request. As described in the docs page: Use Jackson framework as JSON ContentTypeHandler.

    The default JSON Content Handler is build on top of the JSON-lib. If you prefer to use the Jackson framework for JSON serialisation, you can configure the JacksonLibHandler as Content Handler for your json requests.

    First you need to add the jackson dependency to your web application by downloading the jar file and put it under WEB-INF/lib or by adding following xml snippet to your dependencies section in the pom.xml when you are using maven as build system.

    <dependency>
        <groupId>org.codehaus.jackson</groupId>
        <artifactId>jackson-jaxrs</artifactId>
        <version>1.9.13</version>
    </dependency>
    

    Now you can overwrite the Content Handler with the Jackson Content Handler in the struts.xml:

    <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="jackson" class="org.apache.struts2.rest.handler.JacksonLibHandler"/>
    <constant name="struts.rest.handlerOverride.json" value="jackson"/>
    
    <!-- Set to false if the json content can be returned for any kind of http method -->
    <constant name="struts.rest.content.restrictToGET" value="false"/> 
    
    <!-- Set encoding to UTF-8, default is ISO-8859-1 -->
    <constant name="struts.i18n.encoding" value="UTF-8"/>
    

    After that you can use @JsonIgnore annotation.