10

Real situation is like this: Java web server (Weblogic) recieves a request from user for which it has to send a ZIP archive in response. Archive has to be dynamically generated from some files user asked for and one HTML report generated by the server itself. I would like to reuse JSF servlets the server already uses in other cases to generate this report. So, basically, what I use is:

HttpURLConnection  self = new URL ("http://me.myself.com/report.jsf?...").openConnection ();
String  report_html = fetchHtmlFromConnection (self);

and then create the requested ZIP, including the generated HTML in it.

The question is, can I somehow avoid making an internal HTTP request (to report.jsf) in this scenario? That involves basically pointless (since application just "talks" to itself anyway) roundtrips through operating system, HTTPD (which might be on a different machine), etc.

  • Is the report.jsf something that you could move to a class? From some of your comments, the report.jsf is something that you cannot go back and modify correct? I grew up in the jsp world, so I don't really know the limitations of a jsf. – D-Klotz Feb 21 '14 at 22:33
  • 1
    To further clarify where I'm going with this question. If you could move the bulk of the contents of the report.jsf to a set of java classes, then of course you could invoke those classes directly. If there is a lot of markup, then that might be a challenge, or if the report.jsf is a third party widget that can change, then this idea won't be valid. – D-Klotz Feb 23 '14 at 13:57

5 Answers5

2

You should have a business service layer so that a "generateReport" service could be used inside multiple presentation views (even the "zip file" one).

You could do this in standard J2EE way via EJBs or through any custom framework that let you specify injectable business services (e.g. spring).

I think the main problem here is that you can generate the report only through the client interface (http). That makes it an isolated service, not available to other parts of the application. Basically you need a refactoring.

Do not code business inside JSFs or similar. (by the way, try not using jsf at all :D )


  BUSINESS LAYER              PRESENTATION
generateReportService---|---jsf-HtmlReport
                     \__|___
                        |   \ 
someOtherContentService-|----jsf-Zip
Giovanni Caporaletti
  • 5,426
  • 2
  • 26
  • 39
  • This is a mature application and I cannot (and am not going) to rewrite it just because someone suggested so. –  Feb 21 '14 at 16:31
  • Then wrap the existing reporting jsf in a reusable business service, hiding the http connection to internal components. Maybe one day you will have the opportunity to refactor it... – Giovanni Caporaletti Feb 21 '14 at 16:35
  • OK, but how does it solve the problem I have, that to request rendering of a JSF page I have to send an HTTP request? Or do you propose to move report (which *is* presentation, even if presented to user differently than the usual way in browser) out of JSF? –  Feb 21 '14 at 16:40
  • 1
    Well, the report inside a zip file is no more merely presentation but becomes an "artifact" produced by the application and reusable in more than one place. If you move the report out of the jsf, it becomes a service on the business layer. You could think, for example, of a batch job that sends the report in another form (e.g. csv) to an external system. Separate the view generation from the data model. If you cannot do this because it is already written, you could just have a "report file generator" service that produces an html file, and use it as a reusable "content generator" – Giovanni Caporaletti Feb 21 '14 at 16:45
2

I am not very familiar with JSF, but from what I understand of them you can use a technic that is also applicable to JSP pages:

  • Create your own HttpServletResponseWrapper (a class used by the container that lets you modify the response)
  • Use it to override the default Writer (that writes the rendered page to the output) and provide one that writes the output to a String or a temporary file that will feed the compressing code.

There is a nice and simple tutorial that shows you how to do that: http://blog.valotas.com/2011/09/get-output-of-jsp-or-servlet-response.html

Then

  • As hinted by gyan, get a ServletRequestDispatcher from your servlet that will let you invoke the rendering of the JSF
  • Forward the servlet call in order to provide your own HttpServletResponseWrapper
  • Use your HttpServletResponseWrapper to get the rendered HTML and feed it to the zipping code.

So the zipping Servlet would be like:

TempFileRespWrapper respWrapper = new TempFileRespWrapper();
RequestDispatcher dispatcher = getServletContext().getRequestDispatcher( "/report.jsf");
dispatcher.forward(request, respWrapper);
File f = respWrapper.getOutputPath();
addFileToZip(f);
Djizeus
  • 4,161
  • 1
  • 24
  • 42
  • This looks plausible. However, JSF is a bit complicated and I guess I need to do that in a separate thread (at the very least, faces context is thread-local, probably something else too). Is it possible to use or query request dispatcher from a different thread? –  Feb 26 '14 at 18:35
  • I read up a bit about JSF to understand your question, and I think you are right. That is a good question, I don't know if the RequestDispatcher needs to be called from the thread of the initial request. Worth a try :) However, while reading about the FacesContext, I noticed that it has a method to set the output writer. So you could use it in a similar way as what I suggest, and that would probably be safer. Not sure though how to get a FacesContext if you are not already processing a JSF... – Djizeus Feb 27 '14 at 05:56
  • Actually, as I see I don't need current request to retrieve a request dispatcher -- it comes right out of servlet context. So, I will call that (`getRequestDispatcher()`) already in a different thread. Will update on whether this works, but it *looks* like it should. –  Feb 27 '14 at 08:50
  • Testing still pending, didn't have time for it yet. But I hope it will work fine. –  Feb 28 '14 at 18:55
  • Some details should be improved. Take a look in my proposal. – rdllopes Mar 01 '14 at 10:08
  • @doublep, you should avoid additional overhead with an unnecessary temp file. you could consider ZipOutputStream, for example. Another very important thing to note: some important parts of JSF could use Filter chain. In this case, you should specify in this filters digest FORWARD and REQUEST. – rdllopes Mar 01 '14 at 11:21
  • Some feedback from actual implementation. Weblogic, at least, won't accept custom implementations of `HttpServletRequest` (class cast exception) and `HttpServletResponse` (sort of works, but without any output). As a workaround, I subclassed `*Wrapper` and used actual objects provided by the container. After that it worked. –  Mar 04 '14 at 17:55
  • Thanks for the update! Yes I did mean to subclass the `*Wrapper` classes. Goot to know that it worked :) – Djizeus Mar 07 '14 at 15:31
1

Think about request dispatcher strategy, where request/response object would be sent to the report servlet from the entry servlet. In turn report servlet would generate the report and control can be sent to next servlet, which completes the rest of zip and send process.

For constructing a RequestDispatcher object, you can use either the ServletRequest.getRequestDispatcher() method or the ServletContext.getRequestDispatcher() method.

RequestDispatcher dispatcher=getServletContext().getRequestDispatcher( "/report.jsf" );
dispatcher.forward( request, response );

The next servlet set mime type as 'application/zip' and write the zip binary to browser. The user's browser would handle the content in the form of download depending on the browser settings.

Gyanendra Dwivedi
  • 5,511
  • 2
  • 27
  • 53
  • Rather `include()` then, with `forward()` the zipping servlet would never regain control, no? And I also would need a custom response object, since I don't need to send HTML to the "final" response as is, but instead include it in the ZIP. –  Oct 09 '13 at 09:50
  • You do not need to regain the control back, why not to send it to next *new* servlet? Added more content in post. – Gyanendra Dwivedi Oct 09 '13 at 09:57
  • Mostly because I want report generation to just use standard JSF servlet, without any additions. Zipping servlet is special, that's understandable, but report can be just seen as a normal page, with the only difference in the way it is presented to the user --- included in a ZIP, not directly. –  Oct 09 '13 at 10:39
  • So, what is the problem in using standard jsf servlet? As you are saying that it is already existing servlet, just call it from a static html or directly from URL, and your HTML report would get generated on server! – Gyanendra Dwivedi Oct 09 '13 at 10:54
0

Make sure that your web server is configured to cache (generated) html and you will not have to worry about it. First request to full or partial URL will generate a call to jsf generator and later it will be fetched from web server cache.

Yes, there will be a little overhead involved

(your source -> we server -> generator page or cache)

But it is going to be easier in the end.

Wanna Coffee
  • 2,742
  • 7
  • 40
  • 66
Germann Arlington
  • 3,315
  • 2
  • 17
  • 19
0

Tested Solution!

This solution actually get ideas already posted here (specially from @gyan).

  1. Write a Servlet to zip

(you could use an filter for that too. For example, suppose that you have a ZipFilter. You could map the filter for all *.zip and chain that filter with a URLRewrite filter for respective .jsf URL).

public class ZipServlet
    extends HttpServlet {

    @Override
    public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException,
            IOException {

        ZipEntry zipEntry = new ZipEntry("helloWorld.html");
        ZipHttpServletResponseWrapper respWrapper = new ZipHttpServletResponseWrapper(response, zipEntry);
        RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/helloWorld.jsf");
        dispatcher.forward(request, respWrapper);
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "inline; filename=output.zip;");
        response.flushBuffer();
        respWrapper.getOutputStream().close();

    }

}
  • NOTE : Yes, you should use RequestDispatcher

    1. ZipHttpServletResponseWrapper

There's no much to say about that. From Java 7, you can use a native Class to create zip files properly. Using Decorator pattern with ZipOutputStream on top of response.getOutputStream() should be recommended way.

Remember that HttpServletResponseWrapper is a decorator. You should not use that if you don't want to reuse the target "servlet" output directly (you could use an stunt HttpServletResponse rather than use HttpServletResponseWrapper).

public class ZipHttpServletResponseWrapper
    extends HttpServletResponseWrapper {

    private ZipEntry entry;
    private ZipServletOutputStreamWrapper streamWrapper;
    private ZipOutputStream outputStream;
    private PrintWriter printWriter;

    public ZipHttpServletResponseWrapper(HttpServletResponse response, ZipEntry entry) {
        super(response);
        this.entry = entry;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (streamWrapper == null) {
            outputStream = new ZipOutputStream(this.getResponse().getOutputStream());
            outputStream.putNextEntry(entry);
            streamWrapper = new ZipServletOutputStreamWrapper(outputStream);
        }
        return streamWrapper;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (printWriter == null) {
            printWriter = new PrintWriter(getOutputStream());
        }
        return printWriter;
    }

    private class ZipServletOutputStreamWrapper
        extends ServletOutputStream {

        private ZipOutputStream outputStream;

        public ZipServletOutputStreamWrapper(ZipOutputStream outputStream) {
            this.outputStream = outputStream;
        }

        @Override
        public void close() throws IOException {
            outputStream.closeEntry();
            outputStream.finish();
        }

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

}
  1. Now, the secret: mapping wisely!

Some important parts of JSF could use Filter chain (for example, myfaces from Apache use some extensions to provide some JSF components). In this case, you should specify in these filters digest FORWARD and REQUEST

    <filter-mapping>
        <filter-name>extensionsFilter</filter-name>
        <url-pattern>*.jsf</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>    
    </filter-mapping>
rdllopes
  • 4,897
  • 4
  • 19
  • 36