XPages: WebContent Files (3) – Create a Minimizer Servlet

Because of Stefano Fois comment I decided to write an example about how to create a minimizer servlet for Domino which compresses JavaScript resources on the fly. This is, again, a simple Proof-Of-Concept, nothing more and nothing less.

First, I downloaded the YUICompressor, a Java based minimizer for JavaScript code from the project page. There are other compressors outside, I decided to use this one because it was the first result in my StartPage.com search.

The project is a single jar file and can be easily imported into an existing Domino database, in my case to my demonstration NAPI.nsf.

The next step is to create a servlet and a servlet factory, the basics are described here. To enable the servlet, an additional service file is required.

The file com.ibm.xsp-adapter.servletFactory contains the name of the factory class:

ch.hasselba.jsf.servlet.MinimizerServletFactory

The servlet factory is simple, it just defines the servlet class to use and the path which the servlet listens to requests:

package ch.hasselba.jsf.servlet;

import javax.servlet.Servlet;
import javax.servlet.ServletException;

import com.ibm.designer.runtime.domino.adapter.ComponentModule;
import com.ibm.designer.runtime.domino.adapter.IServletFactory;
import com.ibm.designer.runtime.domino.adapter.ServletMatch;

public class MinimizerServletFactory implements IServletFactory {

    private static final String SERVLET_WIDGET_CLASS = "ch.hasselba.jsf.servlet.MinimizerServlet";
    private static final String SERVLET_WIDGET_NAME = "JS Minimizer";

    private ComponentModule module;

    public void init(ComponentModule module) {
        this.module = module;
    }

    public ServletMatch getServletMatch(String contextPath, String path)
            throws ServletException {

        String servletPath = "";

        if (path.contains("/minimizer")) {
            String pathInfo = path;
            return new ServletMatch(getWidgetServlet(), servletPath, pathInfo);
        }

        return null;
    }

    public Servlet getWidgetServlet() throws ServletException {
        return module.createServlet(SERVLET_WIDGET_CLASS, SERVLET_WIDGET_NAME, null);
    }

}

The servlet is now reachable by the following URL

http://example.com/path/to/db.nsf/xsp/minimizer/

How does the minimizer servlet work? It appends all required files into a single string and compresses the string before sending the result to the browser. So the first information needed is which files should be used. This can be done with the parameter „files„, the list of files is concatenated with „+„:

http://example.com/path/to/db.nsf/xsp/minimizer/?files=file1.js+file2.js+ ...

The given files are then loaded via NAPI into a large StringBuffer and then compressed and mimimized by YUI and GZIP.

package ch.hasselba.jsf.servlet;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.Writer;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;

import ch.hasselba.napi.NAPIUtils;

import com.ibm.xsp.webapp.DesignerFacesServlet;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

public class MinimizerServlet extends DesignerFacesServlet implements
        Serializable {

    private static final long serialVersionUID = -1L;

    @Override
    public void service(ServletRequest servletRequest,
            ServletResponse servletResponse) throws ServletException,
            IOException {

        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse res = (HttpServletResponse) servletResponse;
        ServletOutputStream out = servletResponse.getOutputStream();

        try {
            res.setContentType("application/x-javascript");
            res.setCharacterEncoding("utf-8");
            res.addHeader("Content-Encoding", "gzip");

            // load the js requested files
            StringBuffer fileData = new StringBuffer();
            String tmpFile = "";
            String paramFiles = req.getParameter("files");
            String[] files = paramFiles.split(" ");

            // and add them to a buffer
            for (String file : files) {

                try {
                    tmpFile = NAPIUtils.loadFile("DEV01", "NAPI.nsf", file);
                    fileData.append(tmpFile);
                } catch (Exception e) {
                    // ignore errors
                    e.printStackTrace();
                }

            }

            // Compress the JS Code with compressor
            StringWriter sWriter = new StringWriter();
            compress(stringBufferToInputStreamReader(fileData), sWriter);

            // and GZIP it
            ByteArrayOutputStream obj = new ByteArrayOutputStream();
            GZIPOutputStream gzip = new GZIPOutputStream(obj);
            gzip.write(sWriter.toString().getBytes("UTF-8"));
            gzip.close();

            // send it to the client
            out.write(obj.toByteArray());

        } catch (Exception e) {
            e.printStackTrace(new PrintStream(out));
        } finally {
            out.close();
        }
    }

    /**
     * Helper to convert a StringBuffer to an InputStreamReader
     * 
     * @param strBuffer
     *            the StringBuffer to convert
     * @return the converted InputStreamReader
     */
    public static InputStreamReader stringBufferToInputStreamReader(
            final StringBuffer strBuffer) {
        return new InputStreamReader(new ByteArrayInputStream(strBuffer
                .toString().getBytes()));
    }

    /**
     * compresses the JS code using YUI
     * 
     * @param in
     *            the InputStreamReader containing the JS
     * @param out
     *            the Writer Object
     * @throws EvaluatorException
     * @throws IOException
     */
    public static void compress(final InputStreamReader in, Writer out)
            throws EvaluatorException, IOException {
        JavaScriptCompressor compressor = new JavaScriptCompressor(in,
                new ErrorReporter() {
                    public void warning(String message, String sourceName,
                            int line, String lineSource, int lineOffset) {

                        System.out.println("\n[WARNING]");
                        if (line < 0) {
                            System.out.println(" " + message);
                        } else {
                            System.out.println(" " + line + ':' + lineOffset
                                    + ':' + message);
                        }
                    }

                    public void error(String message, String sourceName,
                            int line, String lineSource, int lineOffset) {
                        System.out.println("[ERROR] ");
                        if (line < 0) {
                            System.out.println(" " + message);
                        } else {
                            System.out.println(" " + line + ':' + lineOffset
                                    + ':' + message);
                        }
                    }

                    public EvaluatorException runtimeError(String message,
                            String sourceName, int line, String lineSource,
                            int lineOffset) {
                        error(message, sourceName, line, lineSource, lineOffset);
                        return new EvaluatorException(message);
                    }
                });

        // call YUI
        compressor.compress(out, 0, true, false, false, false);
    }
}

For testing purposes I imported an uncompressed version of jQuery and created a file named helloWorld.js. The helloWorld.js contains a single function only, just to test and verify the outcome of the servlet.

Then, I created a test.html, a simple HTML page which loads the mimimized JavaScript files:

<html>
 <body>
   <h1>Test</h1>
   <script src="./xsp/minimizer/?files=jquery-1.11.1.js+helloWorld.js">
   </script>
 </body>
</html>

This is how the WebContent folder looks like in package explorer:

When opening the test.html page, you can see that only a single request is made to load the data from the servlet:

The size of the response is only 30 % of the original jQuery source request (which is GZIPed too):

When testing the code in the Firebug console, everything is working as expected. The „HelloWorld“ function shows an alert…

and jQuery is defined:

The performance on my test server is very good, without code optimization or caching of the generated JavaScript elements.

Dieser Beitrag wurde unter Java, Java Script, Web, XPages abgelegt und mit , , , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

6 Antworten zu XPages: WebContent Files (3) – Create a Minimizer Servlet

  1. Mark Roden sagt:

    Thanks as always Sven – wow !

    So I bet you can’t make XPages build me a Porsche………..go on I dare you 🙂

  2. Stefano Fois sagt:

    Thanks so much.
    It is what I was trying to do.
    If you can I ask one more step.
    I’m playing with the NAPI and I would like the minimized file is written directly into the WebContent .nsf.
    Making the necessary changes I noticed that the FileAccess.getFileByPath not return null if the file does not exist.
    So how to know if a file already minimized in the WebContent already exists or not (to create or simply update it)?
    thanks again

  3. Fredrik Stöckel sagt:

    Good stuff – nice work. Think I’m gonna try this on the jsx (react.js) stuff I’m playing with atm (for jsx compiler output)

    Fredrik

  4. Stefano Fois sagt:

    One of the problems I am encountering writing .js within the WebContent is that you can not search for text in ALL .js files simultaneously.
    Is there a way with the NAPI to have the name of all files within the WebContent?
    A small example of how a text search on all files within the WebContent would be very welcome.

    This is definitely a great blog.
    Thank you very much.

Schreibe einen Kommentar zu Stefano Fois Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.