Struts2 Rest Mapper Taking Over Non Rest Json Result

1.1k Views Asked by At

Struts Version: 2.5.2

Struts Dependencies in POM

 <dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>${org.strutsframework-version}</version>
</dependency>


<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-convention-plugin</artifactId>
    <version>${org.strutsframework-version}</version>
</dependency>

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-rest-plugin</artifactId>
    <version>${org.strutsframework-version}</version>
</dependency>

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-spring-plugin</artifactId>
    <version>${org.strutsframework-version}</version>
</dependency>

Struts xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
        "http://struts.apache.org/dtds/struts-2.3.dtd">

<struts>
    <!-- Tell jinjava where the templates are -->
    <constant name="struts.jinjava.basepath" value="WEB-INF/jinjava" />
    <!-- custom jinjava tags specific to iws -->
    <constant name="struts.jinjava.scan.tagPackage" value="com.hs.iws.jinjava.tag" />
    <constant name="struts.jinjava.scan.functionPackage" value="com.hs.iws.jinjava.function" />

    <!--Tell struts to use the REST action Mapper-->
    <!--<constant name="struts.mapper.class" value="rest"/>-->

    <!-- allow rest and non rest actions to live together -->
    <constant name="struts.mapper.class" value="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper" />
    <constant name="struts.mapper.prefixMapping" value=":rest,/grid:struts"/>
    <constant name="struts.rest.namespace" value="/" />

    <constant name="struts.convention.action.suffix" value="Action"/>
    <constant name="struts.convention.action.mapAllMatches" value="true"/>
    <constant name="struts.convention.package.locators.basePackage" value="com.hs.iws.actions" />

    <!--re-assert the extensions for struts that have been over written by the rest plugin-->
    <constant name="struts.action.extension" value="xhtml,,json,action"/>
    <constant name="struts.rest.content.restrictToGET" value="false" />

    <!--configure Convention Plugin to find our controllers-->
    <constant name="struts.convention.default.parent.package" value="iws-default"/>

    <!-- Spring Configuration -->
   <!-- <constant name="struts.objectFactory" value="spring" /> -->
    <constant name="struts.objectFactory.spring.autoWire" value="type" />


    <!-- all grid actions should fall under this package -->
    <package name="iws-grid" namespace="/grid" extends="struts-default,jweb-struts-gson-json,jinjava,datatables">
        <interceptors>

            <interceptor-stack name="iws-datatable-stack">
                <interceptor-ref name="exception"/>
                <interceptor-ref name="alias"/>
                <interceptor-ref name="servletConfig"/>
                <interceptor-ref name="i18n"/>
                <interceptor-ref name="prepare"/>
                <interceptor-ref name="chain"/>
                <interceptor-ref name="datetime"/>
                <interceptor-ref name="staticParams"/>
                <interceptor-ref name="actionMappingParams"/>
                <interceptor-ref name="params"/>
                <interceptor-ref name="gson-json" />
                <interceptor-ref name="workflow">
                    <param name="excludeMethods">input,back,cancel,browse</param>
                </interceptor-ref>
                <interceptor-ref name="debugging"/>
            </interceptor-stack>

        </interceptors>

        <default-interceptor-ref name="iws-datatable-stack" />
    </package>


    <package name="iws-default" extends="rest-default, struts-default, jinjava, jweb-struts-gson-json" namespace="/">


    </package>
</struts>

Action Class

package com.hs.iws.actions;

import com.hs.datatables.DataTable10CriteriaQuery;
import com.hs.datatables.DataTable10Helper;
import com.hs.iws.model.Users;
import org.apache.struts2.convention.annotation.*;

/**
 * Created by Paul on 9/14/2016.
 */
@InterceptorRef(value = "iws-datatable-stack")
@ParentPackage(value = "iws-grid")
public class TestGridAction extends DataTable10CriteriaQuery{

    @Action(value="/test-grid-json",
            results={
                    @Result(name = "success", type = "datatable")
            }
    )
    public String execute() {
        return super.execute();
    }

    @Override
    protected Class<?> getHibernateClass() {
        return Users.class;
    }
}

I am working with the DataTables JS library and am trying to write actions for the grid. I have a api in place that creates the json for me already and I just need to stream it back. I created a custom result to handle this, but the result mapped in the action never runs. No matter what the result type, the REST mapper tries to handle it as soon as it sees that application/json has been requested from the client. I have used the prefix mapping in the configuration to have all url's using /grid to bypass the rest mapper. It seems to be working in some capacity because it runs the correct interceptor stack and is using the @Action annotation information to map the url. However, the result specified is NOT running and is instead being provided by the rest mapper based on the stack trace I am receiving. I would like to completely bypass the rest mapper for any actions in the /grid namespace. Have I done something wrong in the configuration that is still causing rest to be involved in the request to those actions?

Stack Trace

ERROR RestActionInvocation Exception processing the result.
 net.sf.json.JSONException: java.lang.reflect.InvocationTargetException
    at net.sf.json.JSONObject._fromBean(JSONObject.java:987)
    at net.sf.json.JSONObject.fromObject(JSONObject.java:168)
    at net.sf.json.AbstractJSON._processValue(AbstractJSON.java:265)
    at net.sf.json.JSONArray._processValue(JSONArray.java:2514)
    at net.sf.json.JSONArray.processValue(JSONArray.java:2539)
    at net.sf.json.JSONArray.addValue(JSONArray.java:2526)
    at net.sf.json.JSONArray._fromCollection(JSONArray.java:1057)
    at net.sf.json.JSONArray.fromObject(JSONArray.java:123)
    at net.sf.json.AbstractJSON._processValue(AbstractJSON.java:237)
    at net.sf.json.JSONObject._processValue(JSONObject.java:2808)
    at net.sf.json.JSONObject.processValue(JSONObject.java:2874)
    at net.sf.json.JSONObject.setInternal(JSONObject.java:2889)
    at net.sf.json.JSONObject.setValue(JSONObject.java:1577)
    at net.sf.json.JSONObject._fromBean(JSONObject.java:934)
    at net.sf.json.JSONObject.fromObject(JSONObject.java:168)
    at net.sf.json.AbstractJSON._processValue(AbstractJSON.java:265)
    at net.sf.json.JSONObject._processValue(JSONObject.java:2808)
    at net.sf.json.JSONObject.processValue(JSONObject.java:2874)
    at net.sf.json.JSONObject.setInternal(JSONObject.java:2889)
    at net.sf.json.JSONObject.setValue(JSONObject.java:1577)
    at net.sf.json.JSONObject._fromBean(JSONObject.java:934)
    at net.sf.json.JSONObject.fromObject(JSONObject.java:168)
    at net.sf.json.JSONObject.fromObject(JSONObject.java:130)
    at org.apache.struts2.rest.handler.JsonLibHandler.fromObject(JsonLibHandler.java:72)
    at org.apache.struts2.rest.DefaultContentTypeHandlerManager.handleResult(DefaultContentTypeHandlerManager.java:181)
    at org.apache.struts2.rest.RestActionInvocation.executeResult(RestActionInvocation.java:227)
    at org.apache.struts2.rest.RestActionInvocation.processResult(RestActionInvocation.java:194)
    at org.apache.struts2.rest.RestActionInvocation.invoke(RestActionInvocation.java:142)
    at com.opensymphony.xwork2.DefaultActionProxy.execute(DefaultActionProxy.java:154)
    at org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:556)
    at org.apache.struts2.dispatcher.ExecuteOperations.executeAction(ExecuteOperations.java:81)
    at org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:113)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
    at com.hs.security.SecurityScanner.doFilter(SecurityScanner.java:95)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:105)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:506)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:620)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:1078)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:760)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1524)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.apache.commons.beanutils.PropertyUtilsBean.invokeMethod(PropertyUtilsBean.java:2116)
    at org.apache.commons.beanutils.PropertyUtilsBean.getSimpleProperty(PropertyUtilsBean.java:1267)
    at org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(PropertyUtilsBean.java:808)
    at org.apache.commons.beanutils.PropertyUtilsBean.getProperty(PropertyUtilsBean.java:884)
    at org.apache.commons.beanutils.PropertyUtils.getProperty(PropertyUtils.java:464)
    at net.sf.json.JSONObject._fromBean(JSONObject.java:918)
    ... 52 more
Caused by: java.lang.UnsupportedOperationException: JsonObject
    at com.google.gson.JsonElement.getAsByte(JsonElement.java:257)
    ... 62 more
1

There are 1 best solutions below

6
Quaternion On

I've run into a similar issue. First, as you probably already know the struts2-rest-plugin can return more than just json, it can return xml, and xhtml as well (by switching on the file extension in the url). The annoying reason that your result does not work is that the strtus2-rest-plugin doesn't use results but uses ContentTypeHandlers which supersede any attempts to use results.

I think in your case, the .action suffix disrupts the rest-plugin and so it goes looking for another matching action, probably using plain old conventions.

In my own code I created a restful and non-restful packages to side step this issue. I also needed to create my own content-type handler to replace their default. It is possible to turn a custom result into a custom-content-type handler, but unless it is of type "xml, json, or xhtml" I think creating a non-restful package, and using that to house those actions makes more sense.

If I have time tonight I'll provide a copy of the struts.xml used to create two sets of packages. This will be useful because I found the struts2-rest-plugin's configuration to be brittle (not very intuitive, and require lines that didn't entirely make sense to me which were added more by guessing than logic).

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
    "http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
    <constant name="struts.devMode" value="true" />
    <!-- the next two lines are ONLY if you want to override a content handler, and since mine is custom it would be with your own impl, however without pain you can only overrride because I think think the extensions are hard coded... so you can't just add your own, could be wrong -->
    <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="flexjson" class="com.kenmcwilliams.s2.result.FlexJsonHandler" />
    <constant name="struts.rest.handlerOverride.json" value="flexjson"/>

    <constant name="struts.action.extension" value="xhtml,,xml,json,action"/>
    <constant name="struts.mapper.class" value="org.apache.struts2.dispatcher.mapper.PrefixBasedActionMapper" />
    <constant name="struts.mapper.prefixMapping" value="/rest:rest,:struts"/>
    <constant name="struts.convention.action.mapAllMatches" value="true"/>
    <constant name="struts.convention.default.parent.package" value="my-conventions"/>
    <constant name="struts.rest.namespace" value="/rest"/>

    <package name="my-conventions" namespace="/"  extends="convention-default" >
        <result-types>
            <result-type name="tiles" class="org.apache.struts2.views.tiles.TilesResult"/>
        </result-types>
        <!-- Following is required for some reason -->
        <global-allowed-methods>execute,input,back,cancel,browse,save,delete,list,index,show,create,update,destroy,edit,editNew</global-allowed-methods>
    </package>

    <package name="my-rest" namespace="/rest" extends="rest-default">
        <result-types>
            <result-type name="flexjson" class="com.kenmcwilliams.s2.result.FlexJsonResult"/>
        </result-types>
    </package>

    <!-- not needed unless you're planning on using tiles -->
    <package name="my-tiles" namespace="/tiles" extends="tiles-default" strict-method-invocation="false">
        <result-types>
            <result-type name="tiles" class="org.apache.struts2.views.tiles.TilesResult"/>
        </result-types>
    </package>
</struts>