1

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
Roman C
  • 49,761
  • 33
  • 66
  • 176
Paul Zepernick
  • 1,452
  • 1
  • 11
  • 25
  • I found out that if I call the action using ".action" it calls the correct result. Not sure if that should be required? Maybe someone can shed more light on that. – Paul Zepernick Sep 20 '16 at 18:09
  • Use some debugger tool – Roman C Sep 20 '16 at 19:56
  • https://struts.apache.org/docs/rest-plugin.html#RESTPlugin-RESTandnon-RESTfulURL%27sTogetherConfiguration. – Aleksandr M Sep 24 '16 at 13:34
  • Questions seeking debugging help ("why isn't this code working?") must include the desired behavior, a specific problem or error and the shortest code necessary to reproduce it in the question itself. Questions without a clear problem statement are not useful to other readers. See: How to create a [Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve). – Roman C Sep 26 '16 at 19:29
  • Thank you Aleksandr, I already am using it. That is what my question is about. I have read the docs over and over and cannot find what I am missing. That is when I asked the question here. It seems to be configured correctly. It is calling the action correctly based off of that, but the RESULT I am calling from the struts action will not run. The rest mapper takes over the result and returns a application/json of it's own when that is the request type. – Paul Zepernick Sep 28 '16 at 12:14
  • @PaulZepernick Didn't noticed `prefixMapping` at first. What happens if you use not custom result? Can you tell more about your result? – Aleksandr M Sep 28 '16 at 19:35
  • @AleksandrM The same thing happens with any result type I put there. The result specified in the annotation never runs. I have tried stream, the struts-json plugin, and now my own custom result. Neither of them ever fired the code in the result specified. The result is ignored and the REST mapper tries to generate application/json. It only happens when the client requests application/json the rest mapper seems to completely take over the result. – Paul Zepernick Sep 29 '16 at 13:56
  • Debug `PrepareOperations#findActionMapping`. See which action mapper is used and which action mapping is being found. – Aleksandr M Sep 29 '16 at 16:40
  • @AleksandrM thanks, I will get back with my findings – Paul Zepernick Sep 29 '16 at 17:56
  • I added an example configuration, that should help, showing how to establish a package which does not use rest-plugin configuration. While still allowing conventions to work under that package (and so annotations will be interpreted correctly). – Quaternion Oct 08 '16 at 00:37

1 Answers1

1

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>
Quaternion
  • 10,380
  • 6
  • 51
  • 102
  • Shouldn't `PrefixBasedActionMapper` handle request before any `ContentTypeHandler`? Yes, you do need separate packages with the `PrefixBasedActionMapper`, but there is no need to create custom content type handler. Agree that plugin configuration isn't very intuitive but copy-pasting from the docs should be enough to achieve what you need. – Aleksandr M Oct 07 '16 at 18:22
  • If you are using the the struts2-rest-plugin, then it will map the action by its conventions, and those conventions do not allow you to override how it generates content by using conventions. The only way to override its behaviour is to use a custom-content-type, that being a huge pain it's clear you _don't_ want rhe rest handler resolving the action. – Quaternion Oct 07 '16 at 18:28
  • I didn't look carefully enough to figure out why the rest-plugin is still handling the result, but as mentioned I'll provide a working example later... the two can be compared. – Quaternion Oct 07 '16 at 18:31
  • That is the question: Why prefix mapper doesn't seem to work for the OP? :) – Aleksandr M Oct 07 '16 at 18:37
  • My currently vapour-ware configuration promises to solve that! You have 7h to beat me ;). No seriously, I couldn't get non-rest working... or if I did rest didn't work... all I remember is there are some seemingly very optional parameters that are not optional at all. – Quaternion Oct 07 '16 at 19:01
  • Just trying to find out what might break prefix mapper. For me it is working fine. AFAIR we already had conversation about that on the mailing list. :) – Aleksandr M Oct 07 '16 at 19:06