3

I have a pretty complex problem about struts2 chaining actions, thanks in advance for your patience reading my problem. I will try my best to describe it clearly.

Below is my struts.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
    "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
    "http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>
    <constant name="struts.enable.DynamicMethodInvocation" value="false" />
    <constant name="struts.enable.SlashesInActionNames" value="true" />
    <constant name="struts.devMode" value="false" />


    <package name="default" extends="struts-default" namespace="/">
        <action name="test" class="com.bv.test.TestAction1" >
            <result name="success" type="chain">y</result>
        </action>

        <action name="x">
            <result name="success">/index.jsp</result>
        </action>

        <action name="y" class="com.bv.test.TestAction2">
            <result name="success">/index.jsp</result>
        </action>
    </package>
</struts>

My logic is like this: When accessing to /myapp/test, TestAction1 will handle the request; In TestAction1, I "include" action x (my 2nd action in my config) like this:

ResponseImpl myResponse = new ResponseImpl(response);
RequestDispatcher rd = request.getRequestDispatcher("/x.action");
rd.include(request, myResponse); 

And the important thing is I am using a customized ResponseIml when including "x.action".

After including, I return "success", so the result chains to action y (3rd action in my config);
And at last, TestAction2 continue to handle the request, it will go to success result, and the jsp should be rendered, but what I see is a blank page.

The jsp file is very simple: index.jsp

<h1>Test!</h1>

My question/puzzle is:

  1. In TestAction1, if I get the response from ServletActionContext, I am getting different ones before and after including; before including is the default response, but after including I got an instance of my customized ResponseImpl; I expect to get the same one: i.e.: the default response;
  2. In TestAction2, I get response from ServletActionContext, what I got is the instance of my customized ResponseIml. This is my most important thing, I think I should get a default response instance here, i.e.: org.apache.catalina.connector.Response, I am running on JBoss;
  3. I am getting a different ActionContext in TestAction2 (compared with the ActionContext I get in TestAction1).

This problem really drive me on the nuts, I have spent days on it.
Any advice will be appreciated!
Thanks a million!!

My Code:

TestAction1:

public class TestAction1 {
  public String execute() {
    ActionContext ac = ActionContext.getContext();
    System.out.println("Before including: the action context is : " + ac);
    HttpServletRequest request = ServletActionContext.getRequest();
    HttpServletResponse response = ServletActionContext.getResponse();
    System.out.println("Before including: the response is : " + response);

    ResponseImpl myResponse = new ResponseImpl(response);
    RequestDispatcher rd = request.getRequestDispatcher("/x.action");
    try {
      rd.include(request, myResponse); 
      String s = myResponse.getOutput();  
      System.out.println("get from response: " + s);
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    ac = ActionContext.getContext();
    System.out.println("After including : the action context is : " + ac);
    response = ServletActionContext.getResponse();
    System.out.println("After including : the response is : " + response);
    return "success";
  }
}

ResponseImpl:

import java.util.Locale;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.Cookie;
import javax.servlet.jsp.JspWriter;

/**
 * 
 * 
 */
public class ResponseImpl extends HttpServletResponseWrapper  {

  //=========================================================
  // Private fields.
  //=========================================================

  private ServletOutputStream outputStream = null;

  private ByteArrayOutputStream byteArrayOutputStream = null;

  private StringWriter stringWriter = null;

  private PrintWriter printWriter = null;

  private HttpServletResponse _response = null;

  private String contentType= "text/html";

  private String encoding = "UTF-8";

  /**
   *
   */
  class ServletOutputStream extends javax.servlet.ServletOutputStream {

    private OutputStream outputStream = null;

    /**
     *
     */
    ServletOutputStream(ByteArrayOutputStream outputStream) {
      super();
      this.outputStream = outputStream;
    }

    /**
     *
     */
    public void write(int b) throws IOException {
      this.outputStream.write(b);
    }
  }

  //=========================================================
  // Public constructors and methods.
  //=========================================================

  /**
   *
   */
  public ResponseImpl(HttpServletResponse response) {
    super(response);
    this._response = response;
  }

  /**
   *
   */
  public String getOutput() {
    if (this.stringWriter != null) {
      return this.stringWriter.toString();
    }

    if (this.byteArrayOutputStream != null) {
      try {
        return this.byteArrayOutputStream.toString(this.encoding);
      }
      catch (UnsupportedEncodingException e) {
      }
      return this.byteArrayOutputStream.toString();
    }

    return null;
  }

  //=========================================================
  // Implements HttpServletResponse interface.
  //=========================================================

  public void addCookie(Cookie cookie) {
  }

  public void addDateHeader(String name, long date) {
  }

  public void addHeader(String name, String value) {
  }

  public void addIntHeader(String name, int value) {
  }

  public boolean containsHeader(String name) {
    return false;
  }

  public String encodeRedirectURL(String url) {
    if (null != this._response) {
      url = this._response.encodeRedirectURL(url);
    }
    return url;
  }

  public String encodeURL(String url) {
    if (null != this._response) {
      url = this._response.encodeURL(url);
    }
    return url;
  }

  public void sendError(int sc) {
  }

  public void sendError(int sc, String msg) {
  }

  public void sendRedirect(String location) {
  }

  public void setDateHeader(String name, long date) {
  }

  public void setHeader(String name, String value) {
  }

  public void setIntHeader(String name, int value) {
  }

  public void setStatus(int sc) {
  }

  public void resetBuffer() {
  }

  //=========================================================
  // Implements deprecated HttpServletResponse methods.
  //=========================================================

  public void setStatus(int sc, String sm) {
  }

  //=========================================================
  // Implements deprecated HttpServletResponse methods.
  //=========================================================

  public String encodeRedirectUrl(String url) {
    return encodeRedirectURL(url);
  }

  public String encodeUrl(String url) {
    return encodeURL(url);
  }

  //=========================================================
  // Implements ServletResponse interface.
  //=========================================================

  public void flushBuffer() {
  }

  public int getBufferSize() {
    return 0;
  }

  public String getCharacterEncoding() {
    return this.encoding;
  }

  public String getContentType() {
    return this.contentType;
  }

  public Locale getLocale() {
    return null;
  }

  public javax.servlet.ServletOutputStream getOutputStream() {
    if (this.outputStream == null) {
      this.byteArrayOutputStream = new ByteArrayOutputStream();
      this.outputStream =
        new ServletOutputStream(this.byteArrayOutputStream);
    }
    return this.outputStream;
  }

  public PrintWriter getWriter() {
    if (this.printWriter == null) {
      this.stringWriter = new StringWriter();
      this.printWriter = new PrintWriter(this.stringWriter);
    }
    return this.printWriter;
  }

  public boolean isCommitted() {
    return true;
  }

  public void reset() {
  }

  public void setBufferSize(int size) {
  }

  public void setCharacterEncoding(String charset) {
  }

  public void setContentLength(int len) {
  }

  public void setContentType(String type) {
    int needle = type.indexOf(";");
    if (-1 == needle) {
      this.contentType = type;
    }
    else {
      this.contentType = type.substring(0, needle);
      String pattern = "charset=";
      int index = type.indexOf(pattern, needle);
      if (-1 != index) {
        this.encoding = type.substring(index + pattern.length());
      }
    }
  }

  public void setLocale(Locale locale) {
  }
}

TestAction2:

public class TestAction2 {

  public String execute() {
    ActionContext ac = ActionContext.getContext();
    System.out.println("In TestAction 2 : the action context is : " + ac);

    HttpServletResponse response = ServletActionContext.getResponse();
    System.out.println("In TestAction 2 : the response is : " + response);
    return "success";
  }
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Struts2 Application</display-name>

     <filter>
        <filter-name>struts2</filter-name>
        <filter-class>
            org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>INCLUDE</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
</web-app>

This is my debug info.

  • Before including: the action context is :com.opensymphony.xwork2.ActionContext@c639ce
  • Before including: the response is : org.apache.catalina.connector.ResponseFacade@8b677f
  • get from response: <h1>Test!</h1>
  • After including : the action context is : com.opensymphony.xwork2.ActionContext@2445d7
  • After including : the response is : com.bv.test.ResponseImpl@165547d
  • In TestAction 2 : the action context is :com.opensymphony.xwork2.ActionContext@19478c7
  • In TestAction 2 : the response is : com.bv.test.ResponseImpl@165547d

So, I have different ActionContext instances before and after the including!!

Roman C
  • 49,761
  • 33
  • 66
  • 176
LiuWenbin_NO.
  • 1,216
  • 1
  • 16
  • 25
  • Please let me know if you need more info, thanks!! – LiuWenbin_NO. May 04 '13 at 10:23
  • What is your `struts.xml`? – Roman C May 04 '13 at 13:04
  • @RomanC, thanks for your comment, I added my struts.xml, please let me know if you need more info. Thanks again! – LiuWenbin_NO. May 04 '13 at 13:52
  • Look at [this](http://stackoverflow.com/questions/16119925/how-to-forward-request-from-servlet-to-action-of-struts2) question, may be it shed a ray of light into the dark realm. – Roman C May 04 '13 at 14:03
  • @RomanC, thanks for your comment. I have `INCLUDE` in my web.xml. So the included action can be executed. Could you do me more favor on it? Thanks a lot! – LiuWenbin_NO. May 04 '13 at 14:07
  • I do not think like that I throw myself out of the abyss the abyss and I can not stand still. – Roman C May 04 '13 at 14:11
  • @RomanC, thanks all the same! And if you have time, please give it a shot if you like. I do think you will have excellent advice on it. Thanks very much! – LiuWenbin_NO. May 04 '13 at 14:14
  • My thoughts that you overriding response in some way used by the dispatcher. You should probably not do it, it really needs to debug it to get the final answer. – Roman C May 04 '13 at 14:18
  • @RomanC, thanks a lot for your patience! I do need to override response, because I put all of the include response to StringWriter, but not display it on the page directly. I need to collect all of the includes responses and display them all in one page on my production code. Thanks again! – LiuWenbin_NO. May 04 '13 at 14:24
  • And, BTW, this code works well for years with struts (1). But migrating to struts2, it stops working. – LiuWenbin_NO. May 04 '13 at 14:30
  • I'm not sure if your response wrapper is working at all. Why the output stream doesn't use `StringWriter`? – Roman C May 04 '13 at 15:17
  • @RomanC, first of all,thanks for your patience! OK, as I mentioned in my previous comment, the response wrapper did work with struts1 for many years. I am migrating it to struts2. As for your question, `why the output stream doesn't use StringWriter`, to be frank, I am not sure about it too, all I have known is, when calling request dispatcher to include another page, we put all of the response string into `StringWriter`, and put the `StringWriter` to `request` as an attribute, then we display the content of `StringWriter` in jsp. – LiuWenbin_NO. May 04 '13 at 20:28
  • @RomanC, continue with former, we display the `StringWriter` in this way: `<% java.io.Writer writer = (java.io.Writer) request.getAttribute(REQ_STRINGWRITER_ATTR); %> <%= writer %>`. And for more info, we have a fully customized tags at server side (not like tld in jsp level), all of the tags will be translated to HTML at runtime, and put the translated HTML string into `StringWriter`, then display it in jsp. For the ResponseWrapper, it is for handling our case ``. I am not sure whether this is helpful, FYI. Sorry I' not sure what's the root problem is. Plz help, thanks! – LiuWenbin_NO. May 04 '13 at 20:35
  • +1 for effort. That said... I'm not sure you're approaching the problem correctly. What is the effect/use-case you are trying to achieve? I have a feeling there are tools or methods to achieve this that is easier than trying to render the view from within the action class (model). You can use the "action tag" to render a view, you can create a custom result type (which seems like a better place for the type of code you are showing). You can use tiles to assemble complicated views (even dynamically now). Just feel there is a better way if we understood what you are doing. – Quaternion May 04 '13 at 21:12
  • @Quaternion, GREAT! I think you are almost understand what I am doing! Yes, I just want to do **include** another action at runtime. For sure, I can use tiles to get the effect I want. But for my case, I am migrating struts app to struts2. Back to my production code, we have an internal framework, namely XFlow (it consists of Action and XForm), each xform is a page, it works like this, all urls go to the same struts action, then from the url, the struts action has a prameter to get whcich xflow is to executed, so the struts action will execute the Action in XFlow and then the XForm. – LiuWenbin_NO. May 04 '13 at 23:20
  • Continue with former, when the XForm being exeucted, it may contains other XFlows, for example, in our XForm, if our engine see something like ``, the engine will try to include the header.xflow, as for the incude is just what I need to make it work. In my template.xform, there are 3 include tags, it includes headere, body and footer. So for my case the template is not controlled by tiles, so I have to insist on working this out. As in my post, I pasted some debug info, you see, in one request, I got DIFFERENT ActionContext instances before and after including. Wierd – LiuWenbin_NO. May 04 '13 at 23:27
  • Quaternion, for my XForm, it cannot be shown directly, our engine will compile XForms, and translate them into HTML (as StringWriter object), and then put the transalated HTML string into request attributes, so in jsp file, it just read the HTML string and print it out, so the page can be seen. – LiuWenbin_NO. May 04 '13 at 23:32
  • Thanks a lot for `Roman C, Quaternion, Dev Blanked`, I worked it out now. The solution is add ActionContext.setContext(actionContext) at the end of TestAction1's execute(). It looks like: ActionContext ac = ActionContext.getContext();...rd.include(..); ... ActionContext.setConext(ac);, it works for me now. I post it here for others' infomation. Thanks again for your advices & kind heart. Have a nice day!! – LiuWenbin_NO. May 06 '13 at 05:23

2 Answers2

1

When you do rd.include another request is fired internally inside the web server. So from struts point of view it sees a completely new request and a new action context is created as a result. (that's why you need to include 'INCLUDE' thing on the struts2 filter.. so that it's seeing included requests as well). Probably since thread local variables are used to track action context and all that when you do ActionContext.getContext() the context related to the new request (related to the include) gets retrieved.

Did you try resetting the response to the initial one in a finally block like below

try {
      rd.include(request, myResponse); 
      String s = myResponse.getOutput();  
      System.out.println("get from response: " + s);
    }
    catch (Exception e) {
      e.printStackTrace();
    } finally {
       ServletActionContext.setResponse(response);
    }

If this resolves the response issue.. you could probably store the string 's' as a variable in the action context and retrieve it inside your Action2

Dev Blanked
  • 8,555
  • 3
  • 26
  • 32
  • Thanks a lot for your answer! I did try this approach, it fixed part of my issue. But I am still not pleased with this approach. To be detailed, after the including action finishes its executing, it returns to TestAction1, at this point, if I tried to get the ActionContext by calling ActionContext.getContext(), it returns a different one from before including executes. So the `response` is different too, in TestAction2, I got different `response` even if I have set the original response back to ServletActionContext. I am afraid I cannot make myself understood fo this complex issue. Thanks – LiuWenbin_NO. May 05 '13 at 14:23
  • Because after including, if I get response from ServletActionContext, it is a different one, i.e. it is an instance of my customized ResponseImpl. And further more, when I get response from TestAction2, I am also get an intance of my customized ResponseImpl. This caused the page cannot be shown, if I can get the original response in TestAction2, the page can be shown. – LiuWenbin_NO. May 05 '13 at 14:26
  • I think when after including finishes, it back to the TestAction1, the `ActionConext.getContext()` should be the same instance before including, but it isn't the same one. How this comes about? Before and after including should be in the same request and thread, and I think they should be the same one, why not? Thanks a lot for your patience!! – LiuWenbin_NO. May 05 '13 at 14:36
  • Now I think this problem could be fixed if I can get the original Response and ActionContext instance after including, and in TestAction2, I can get the original Response other than the instance of my customized ResponseImpl. This is my direction now. I am still in struggle for this issue. Thank you!! For any advice! – LiuWenbin_NO. May 05 '13 at 14:52
  • @LiuWenbin_NO. so setResponse(originalResponse) didn't work is it ? – Dev Blanked May 05 '13 at 14:57
  • No, it doesn't work. Because in the action chaining, i.e. in TestAction2, there is a different ActionContext instance, so when executing TestAction2, the response object is not the original one (org.apache.catalina.connector.ResponseFacade), but it is an instance of my ResponseImpl, which cause the page cannot be rendered. Thank you! – LiuWenbin_NO. May 05 '13 at 15:05
  • In other words, although I `setResponse(originalResponse)` in TestAction1, I cannot get it in TestAction2. Because the different ActionContext between them. – LiuWenbin_NO. May 05 '13 at 15:07
0

You could also try the following as well. Instead of using chaining.. inside your TestAction1 include the TestAction2 with the original response. return 'none' from the action as the return value.

public class TestAction1 {
  public String execute() {
    ActionContext ac = ActionContext.getContext();
    System.out.println("Before including: the action context is : " + ac);
    HttpServletRequest request = ServletActionContext.getRequest();
    HttpServletResponse response = ServletActionContext.getResponse();
    System.out.println("Before including: the response is : " + response);

    ResponseImpl myResponse = new ResponseImpl(response);
    RequestDispatcher rd = request.getRequestDispatcher("/x.action");
    try {
      rd.include(request, myResponse); 
      String s = myResponse.getOutput();  
      System.out.println("get from response: " + s);
    }
    catch (Exception e) {
      e.printStackTrace();
    }



      RequestDispatcher rd = request.getRequestDispatcher("/y.action");
        try {
          rd.include(request, response); 
        }
        catch (Exception e) {
          e.printStackTrace();
        } finally {
          return "none";
        }
      }
    }
Dev Blanked
  • 8,555
  • 3
  • 26
  • 32
  • @LiuWenbin_NO. did u give a try using this approach – Dev Blanked May 05 '13 at 14:55
  • 1
    thanks for your approach. I didn't try this approach, because I will change very very much of my current application even this works for me. But now, I worked it out! Just add `ActionContext.setContext(actionContext)` at the end of TestAction1's execute(). It looks like: `ActionContext ac = ActionContext.getContext();...rd.include(..); ... ActionContext.setConext(ac);`, it works for me now. – LiuWenbin_NO. May 06 '13 at 05:21