8

I'm struggling with the following situation:

In our current web application running on Tomcat 7.0.64, we manage to include a JSP page via Java with the help of an own class CharArrayWriterResponse implementing HttpServletResponseWrapper.

The reason for doing so is that we wrap the resulting HTML into JSON needed for an AJAX Response.

Dependencies:

<dependency>
     <groupId>javax</groupId>
     <artifactId>javaee-web-api</artifactId>
     <version>7.0</version>
     <scope>provided</scope>
</dependency>
<dependency>
     <groupId>javax.servlet</groupId>
     <artifactId>jstl</artifactId>
     <version>1.2</version>
</dependency>

Code example:

// somewhere in servlet doPost()/doGet()
try (PrintWriter out = response.getWriter()) {
     out.println(getJspAsJson(request, response));
}

private static String getJspAsJson(HttpServletRequest request, HttpServletResponse response) {
    String html = getHtmlByJSP(request, response, "WEB-INF/path/to/existing.jsp");
    Gson gson = new GsonBuilder().disableHtmlEscaping().create();
    return "{\"results\":" + gson.toJson(html) + "}";
}

public static String getHtmlByJSP(HttpServletRequest request, HttpServletResponse response, String jsp) {
     CharArrayWriterResponse customResponse = new CharArrayWriterResponse(response);
     request.getRequestDispatcher(jsp).include(request, customResponse);
     return customResponse.getOutput();
}

public class CharArrayWriterResponse extends HttpServletResponseWrapper {
    private final CharArrayWriter charArray = new CharArrayWriter();

    public CharArrayWriterResponse(HttpServletResponse response) {
        super(response);
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        // this is called ONLY in tomcat
        return new PrintWriter(charArray);
    }

    public String getOutput() {
        return charArray.toString();
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        // this is called ONLY in WebLogic
        return null; // don't know how to handle it
    }
}

Hint: I didn't consider exception handling in above code samples.

I have to migrate this application to WebLogic (12.2.1) but this solution is not working anymore.

What I found out so far:

In Tomcat after the call to request.getRequestDispatcher(jsp).include() of the example above getWriter() of my CharArrayWriterResponse class is called.

In WebLogic getWriter() is not called anymore and that's the reason why it doesn't work anymore.

After some debugging, I found out that in WebLogic instead of getWriter() only getOutputStream() is called if I override it. getWriter() is not called once on Weblogic so there have to be differences in the underlying implementation of Tomcat and WebLogic.

Problem is that with getOutputStream() I see no possibility to get the response of the include() call in a separate stream or something else and to convert it to String for usage to build the final JSON containing the HTML.

Has someone solved this problem already and can provide a working solution for including a JSP in a programmatic way in combination with WebLogic?

Does anyone know another solution to achieve my goal?

Thanks for suggestions.


Solution

See working example here

Hint

A difference I found out between Tomcat and new Weblogic solution: With latter one it's not possible to include JSPF's directly anymore wheras with Tomcat getWriter() it is.

Solution is wrapping the JSPF inside a JSP file.

Community
  • 1
  • 1
Michael
  • 189
  • 1
  • 17

3 Answers3

2

I did this:

@Override
public ServletOutputStream getOutputStream() throws IOException {
    // this is called ONLY in WebLogic
    // created a custom outputstream that wraps your charArray
    return new CustomOutputStream(this.charArray);
}

// custom outputstream to wrap charArray writer
class CustomOutputStream extends ServletOutputStream {

    private WriterOutputStream out;

    public CustomOutputStream(CharArrayWriter writer) {
        // WriterOutputStream has a constructor without charset but it's deprecated, so change the UTF-8 charset to the one you use, if needed
        this.out = new WriterOutputStream(writer, "UTF-8");
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {
    }

    @Override
    public void write(int b) throws IOException {
        this.out.write(b);
        this.out.flush(); // it doesn't work without flushing
    }
}

I used WriterOutputStream from apache commons-io, so I had to include in my pom.xml:

<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.5</version>
</dependency>

I don't know what's in your jsp file, but I've tested with a simple one and I believe it worked. My jsp file:

<b>Hello world</b>

<p>testing</p>

<ul>test
<li>item</li>
<li>item2</li>
</ul>

Output (when accessing the servlet in a browser):

{"results":"<b>Hello world</b>\n\n<p>testing</p>\n\n<ul>test\n<li>item</li>\n<li>item2</li>\n</ul>"}
  • 3
    Hugo's solution is well oriented, but it misses an important detail: Method `getOutputStream` might be called **more than once** (which is what it is probabily happening in this case, since there are several URIs implied in the same chain). So, care must be taken to create a `CustomOutputStream`object *just once* and **store it** to be returned in the further calls to `getOutputStream`. – Little Santi Mar 01 '17 at 11:37
  • 1
    @Hugo Now it's working, thanks again for all your effort, Problem was that I missed the call to `flush()` in the `write()` method ;-). Anyway the solution is not very fast so I'm looking into improving it further like you mentioned with BufferedOutputStream`. I'll provide the solution as **answer** here if it's finished. – Michael Mar 01 '17 at 12:32
  • Michael, you're welcome. You could also follow the suggestion made by @LittleSanti, I noticed that it really happens (lots of CustomOutputStream objects created), so there's also room for improvement at this point. –  Mar 01 '17 at 12:42
  • 1
    And, if you use a BufferedOutputStream, remember to give it a size big enough (multiple of 4Kb is recommended, minimum 4Kb). – Little Santi Mar 01 '17 at 13:25
2

Here is my updated and working example how to include a JSP file programmatically if getOutputStream() instead of getWriter() is called when implementing HttpServletResponseWrapper:

public class MyServletOutputStream extends ServletOutputStream {

    private final BufferedOutputStream bufferedOut;

    public MyServletOutputStream(CharArrayWriter charArray) {
        this.bufferedOut = new BufferedOutputStream(new WriterOutputStream(charArray, "UTF-8"), 16384);
    }

    @Override
    public void write(int b) throws IOException {
        this.bufferedOut.write(b);
    }

    /**
     * This is needed to get correct full content without anything missing
     */
    @Override
    public void flush() throws IOException {
        if (this.bufferedOut != null) {
            this.bufferedOut.flush();
        }
        super.flush();
    }

    @Override
    public void close() throws IOException {
        this.bufferedOut.close();
        super.close();
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {
    }
}

public class CharArrayWriterResponse extends HttpServletResponseWrapper {

    private final CharArrayWriter charArray = new CharArrayWriter();
    private ServletOutputStream servletOutputStream;

    public CharArrayWriterResponse(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (servletOutputStream == null) {
            servletOutputStream = new MyServletOutputStream(this.charArray);
        }
        return servletOutputStream;
    }

    public String getOutputAndClose() {
        if (this.servletOutputStream != null) {
            try {
                // flush() is important to get complete content and not last "buffered" part missing
                this.servletOutputStream.flush()
                return this.charArray.toString();
            } finally {
                this.servletOutputStream.close()
            }
        }
        throw new IllegalStateException("Empty (null) servletOutputStream not allowed");
    }

    // not necessary to override getWriter() if getOutputStream() is used by the "application server".
}

// ...somewhere in servlet process chain e.g. doGet()/doPost()
// request/response The original servlet request/response object e.g. from doGet/doPost(HttpServletRequest request, HttpServletResponse response)
CharArrayWriterResponse customResponse = new CharArrayWriterResponse(response);
request.getRequestDispatcher("/WEB-INF/path/to/existing.jsp").include(request, customResponse);
String jspOutput = customResponse.getOutputAndClose();
// do some processing with jspOut e.g. wrap inside JSON

// customResponse.getOutputStream() is already closed by calling getOutputAndClose()
Michael
  • 189
  • 1
  • 17
  • @Little Santi/@Hugo The solution above is working but I'm not 100% sure if my calls to `flush()` are correct. I also found this [answer](http://stackoverflow.com/a/8972088/7617251) where `flushBuffer()` is also implemented. Maybe you both can double check this solution, thanks. – Michael Mar 02 '17 at 14:42
  • 1
    I'd try to call `flush` in `getOutput` method, or override `close` method in `MyServletOutputStream` (including a call to `flush` before closing it). –  Mar 02 '17 at 16:11
  • Another small question regarding the close(): Do I need a `customResponse.getOutputStream().close();`call in finally block around the `include()` or is it called "automagically" with the `close()` of the real response? – Michael Mar 03 '17 at 13:07
  • I'm not sure, but in my test I didn't need to do it (I just did [this](http://stackoverflow.com/questions/42440437/issue-with-requestdispatcher-including-jsp-programmatically-in-weblogic-12c/42562024#42562024) and it worked). You could override `close` and put a System.out.println just to see if it's called (but I believe it is, although I'm not sure) –  Mar 03 '17 at 13:09
  • With overriden `close()` and breakpoint set inside it's never called. To release the resources correctly I think it's better to call `customResponse.getOutputStream().close();`like mentioned above. @Little Santi What do you think about this? – Michael Mar 03 '17 at 13:34
  • 1
    My suggestions is to call close in `getOutput` method (inside a `finally` block, after `return` clause) Like this: `try { return this.charArray.toString(); } finally { servletOutputStream.close(); }` Note that I didn't need to call `flush` inside `getOutput` (like you did), for me it worked without it. –  Mar 03 '17 at 14:51
  • 1
    Updated my answer again by overriding close() and calling it in `getOutputAndClose()` (renamed from `getOutput()`. Also removed called to `flush()` in `getOutputStream()`. I think you don't need the `flush()` call in `your `getOutput()`cause in your solution you call it in `write()` everytime. – Michael Mar 03 '17 at 15:35
2

As I can't figure out how to put multi-line code inside comments, I'm just putting it here. I could fix the issue by overriding flush() method in MyServletOutputStream class:

// inside MyServletOutputStream class
@Override
public void flush() throws IOException {
    if (this.bufferedOut != null) {
        this.bufferedOut.flush();
    }
    super.flush();
}
Community
  • 1
  • 1