Saturday, July 28, 2012

PLUPLOAD and JSF and PrimeFaces

I do software for a living.   Generally, I do enterprise oriented Java web applications and internet applications.

Java & EE software can take up lots of 'horse power' and that is part of the reason why I chose to buy a Chaos 1212 and run Fedora 17.  Other reasons, was I like small stuff with big punch and I appreciate good hardware and I tend to buy way over my needs and hope it takes longer to grow out of.    But that is not what I was going to gab about.

A Problem

My current project is an EE6 web application project using JSF 2.x and PrimeFaces (we are using the latest GA version of PF - 3.3.1) .  One of the features we have is that users can upload and attach images and documents (i.e. artifacts) to some of the data they create.    Not really a big deal; uploading and storing or otherwise associating file artifacts with user created data is nothing new and a well traveled area.  However, for some reason, it has become really important to my business stake holders that end users be able to select multiple files at one time and push the 'go' button and have the application upload a batch of files.  Still (seemingly) not particularly hard.  

This application is the first foray into the 'Rich Browser Web Application' space for the company I am working for.    This means they want fancy stuff with ajax and lots of active (i.e. javascript)  stuff and it to be highly stylized and they want it to run on a variety of browsers; including IE 7 and above.     This is where things get complicated.....some browsers don't support multiple selection input/file elements.


PLUPLOAD 

There are several uploader helper widgets available.  You can go google the alternatives for your self; there are a lot of good ones out there but plupload seems to have the right combination of features to match the projects needs.   One trouble spot with plupload is that it doesn't have any sort of intrinsic support for back end JSF applications that wish to 'catch the files' uploaded to plupload.  But it is just HTTP that is sending the files, right?   Should be easy to wrap, tweak or hack plupload to send to a JSF app, correct?

p:fileUpload

Primefaces has an OK uploader control, but it doesn't suit my projects needs entirely.   Actually it works just fine and it ultimately is just an <input type='file' /> tag; and there just isn't a good way to get IE and Sarfari (windows) to allow the user to pick multiple files at once.  At any rate, it works by having a servlet filter intercept all requests; deal with MULTIPART requests using Apache Commons FileUpload and  Commons IO and then allowing the modified request be dispatched to the Faces Servlet for JSF life cycle processing.   It works somewhat naturally in the JSF & PrimeFaces world; just check out the PF demo site and code.

My Strategy

Simple, just have the on page PLUPLOADER widget, do what  the PF <p:fileUpload  /> does and then the JSF/PF uploader filter will take care of  the rest.  So to understand better what the PF control does,  I figured I'd use some sort of proxy to capture the raw requests and compare that to what pluploader does.  

Long story short, capturing the normal PF upload stuff easy  (burp, firebug, webscarab, and many others) all expose requests OK.     Capturing requests from pluploader is much harder and as it turns out, at least for the flash run-time from pluploader, darn near impossible (for me -- I am sure wireshark could have helped).  After much effort and little progress,  I ultimately aborted  this strategy out of frustration and because of this StackOverload post and realized that maybe I was trying to use pluploader wrong.

My First Glimpse of Success

There are many good resources on using pluploader with a java back-end (to 'catch' the uploaded files); just go look for your self.  In particular I found this blog post one of the more useful ones:  http://blog.shadit.com/2010/10/28/java-servlet-plupload/.  Armed with a servlet to catch files, and the above SO post, a working proto-type emerged:
  1. drop a plupload instance onto a JSF page along with the needed .js files and the jquery/js code to start a plupload operation
  2. add a servlet to my PF war to catch the files
  3. add a p:remoteCommand to call a backing bean once plupload was done.
  4. provide some JS glue code to submit to a plupload instance during construction to trigger the the p:remoteCommand as well as attach some request parameters. 
  5. code the action listener (target of the p:remoteCommand) on the backing bean to do what I need to do with the file

Composite Component

After the working proto-type was complete, I needed to make a JSF 2.0 style composite component.   In my current webapp, I had a CC the was built using <p:fileUpload /> and I figured that I can build an almost drop in replacement CC wrapping plupload.     Here are a few things I learned along the way:
  • visual flash object in <div /> that will be hidden and shown (I was embedding plupload in a <p:overlayPanel />) based on user interactions, doesn't always work.  In some browsers the hide and show works just fine, but in others...
  • because of the above and some concerns of making the plupload L & F mesh with the rest of our application, I choose plupload in 'custom' mode where it does the file transport, but doesn't display a UI of any sort and you provide your own JS/dhtml UI displays in the plupload lifecycle js callbacks. 
  • JavaScript closures work well  for dealing with multiple instances of a CC having javascript, that will be on a single view/page.  JavaScript is not my forte; however these links were enough to get me to a solution that works:  JSF 2 fu: Best practices for composite componentsJavaScript Closures 101- they're not magic, oh and this too. 
  • IE sucks.   But you may know that already.   Since I was using a custom model with pluploader - which is basically just using the core api -- I had made my plupload 'container' a table.   IE has issues using innerHTML and various <table /> html tags and plupload does use some javascript that puts innerHTML on the LHS -- IE chokes.   Read more about this here.   This was a massive time waster and I wasn't really obvious at first what the problem was. 
  • I couldn't get the unique file name stuff to work while catching the file in my servlet.   I found I had to attach an additional param to the HTTP request while plupload was sending the file.   I used this to control file name unique-ness.  
  • Back to JSF composite components for a minute:   in my real project I have the pfplupload CC dropped into a view inside a repeating construct.  It seems that the #{cc.id} doesn't change per iteration.   Since I wanted to keep the p:remoteCommand declarations unique I had been giving them a name "rc#{cc.id}".   The net result; my p:remoteCommands didn't have unique java script names.    I never found a good solution for this; I had wanted to find something within cc. or #{composite. } or something within EL or such that would allow me to make my p:remoteCommand names unique, but not really expose this issue to the caller.   In the end I had to make another cc:attribute and allow the caller provide some  instance value that I could use to insure uniqueness.   I made it optional and it defaults to #{cc.id}; when you don't need it.
I will have some more content on this shortly and hopefully a few days or so I will post some sample source as well as war & project.

After a long wait....
For the 1 person who asked for source, sorry for the long delay -- work and life caught up to me and I havent' had time to share things as much as I'd like.   However, I think I have a good working copy of an a PF-based/PLUPLOAD based uploader.    Here it is:

<ui:component
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:p="http://primefaces.org/ui"
    xmlns:ez="http://java.sun.com/jsf/composite/ezcomp"
    xmlns:cc="http://java.sun.com/jsf/composite" >

    <cc:interface>
        <cc:attribute name="renderAsButton" type="java.lang.Boolean" default="false"  shortDescription="if true renders this actuator as a p:commandButton otherwise p:commandLink" />
        <cc:attribute name="buttonStyleClass" type="java.lang.String" shortDescription="style class used when uploader is rendered as button" />
        <cc:attribute name="selectBtnStyleClass" type="java.lang.String" shortDescription="style class used for select button" />
        <cc:attribute name="uploadBtnStyleClass" type="java.lang.String" shortDescription="style class used for upload button" />
        <cc:attribute name="panelStyleClass" type="java.lang.String" shortDescription="style class used for the overlay panel component" />
        <cc:attribute name="triggerLabel" required="true" type="java.lang.String" shortDescription="label on actuator/trigger that displays the uploader" />
        <cc:attribute name="width" type="java.lang.Integer" default="300" shortDescription="width of upload panel" />
        <cc:attribute name="height" type="java.lang.Integer" default="100" shortDescription="heigth of upload panel" />
        <cc:attribute name="targPath" required="true" type="java.lang.String" shortDescription="target path of servlet/thing that will catch the uploaded bytes" />
        <cc:attribute name="max_file_size"  type="java.lang.String" default="50mb"  shortDescription="max file size to allow during uploads" />
        <cc:attribute name="selectBtnLabel" type="java.lang.String" default="Browse"  shortDescription="Lable to use for select files button" />
        <cc:attribute name="uploadBtnLabel" type="java.lang.String" default="Upload" shortDescription="Lable of button that starts pushing/uploading all the files in list" />
        <cc:attribute name="filterLabel" type="java.lang.String" default="Files" shortDescription="simple label displayed in file selectiong dialog" />
        <cc:attribute name="fileFilter" type="java.lang.String" default="*" shortDescription="comma separator list of extensions to include, who knows if wild cards work" />
        <cc:attribute name="additionalParams" type="java.lang.String" shortDescription="String containing JSON formated data to be used as additional request params send to server" />
        <cc:attribute name="heading" type="java.lang.String" default="Select files and then upload" shortDescription="String containing JSON formated data to be used as additional request params send to server" />
        <cc:attribute name="imagePrefix" type="java.lang.String" default="images" shortDescription="Value to prepend to image references that provide upload feed-back to user." />
        <cc:attribute name="resourcePrefix" type="java.lang.String" default="resources/js" shortDescription="Value to prepend to image references that provide upload feed-back to user." />
        <cc:attribute name="allowMultiSelect" type="java.lang.Boolean" default="true" shortDescription="indicator if the file selection should allow for multiples or not." />
        <cc:attribute name="perFileUpdate" type="java.lang.String" required="false" shortDescription="Per file update target string " />
        <cc:attribute name="perFileSuccessCB" method-signature="void perFileUploadSuccess()" required="false" shortDescription="Success callback made per uploaded file (this is almost mandatory)" />
        <cc:attribute name="batchSuccessCB" method-signature="void batchUploadSuccess()" required="false" shortDescription="Whole batch success callback (this is optional but if not used, perFileSuccessCB should be used -- chaos will ensue if you don't use either) " />
        <cc:attribute name="batchUpdate" type="java.lang.String" required="false" shortDescription="Whole batch update target string (not implemented)" />
        <cc:attribute name="instanceId" type="java.lang.String" shortDescription="unique string used to in ensure uniqueness of stuff in cases where this is used in some repeating construct -- in single uploader case you may no need -- but if you have more than one uploader in some sort loop/repeate/table you may want to specify this." />
        <cc:attribute name="immediate" required="false" type="java.lang.Boolean" default="false" shortDescription="control validation and immediateness for any/all CB remote command"/>
        <cc:attribute name="onstart" required="false" type="java.lang.String" shortDescription="Client Side call-back trigger when the upload batch is starting" />
        <cc:attribute name="oncomplete" required="false" type="java.lang.String" shortDescription="Client Side call-back trigger when the upload batch is complete" />
        <cc:attribute name="waitMessage" required="false" type="java.lang.String" default="Please wait" shortDescription="wait message displayed after upload and during bactchcomplete callback"  />
    </cc:interface>

    <cc:implementation>
        <h:outputScript name="plupload.full.js" library="js" target="head" />
        <h:outputScript name="pfPlupload.js" library="js" target="head" />
        <ui:param name="salt" value="#{empty cc.attrs.instanceId ? cc.id : cc.attrs.instanceId }" />
        <ui:param name="triggerId" value="#{'tr'.concat( cc.attrs.renderAsButton ? 'b':'l' )}" />
        <ui:param name="h" value="#{cc.attrs.height}" />
        <ui:param name="w" value="#{cc.attrs.width lt 300 ? 300 : cc.attrs.width}" />
        <!-- get the file list panel height subtract padding and title and button row / file row and * by file row -->
        <ui:param name="hfl" value="#{ ((h - 10 - 14 -14) / 12) * 12 }" />
        <ez:pleaseWaitDlg widgetVar="plw#{salt}" appendToBody="true" pleaseWaitTxt="#{cc.attrs.waitMessage}" />

        <script type="text/javascript"  >
// <![CDATA[
    var cfg_ou#{salt} =
    {
        plup_panel_id       : '#{cc.clientId}:plP',
        file_list_id        : '#{cc.clientId}:fl',
        start_button_id     : '#{cc.clientId}:sf',
        browse_button_id    : '#{cc.clientId}:bf',
        target_url          : '#{cc.attrs.targPath}',
        filter_label        : '#{cc.attrs.filterLabel}',
        filter              : '#{cc.attrs.fileFilter}',
        imagePrefix         : '#{cc.attrs.imagePrefix}',
        resourcePrefix      : '#{cc.attrs.resourcePrefix}',
        maxFileSize         : '#{cc.attrs.max_file_size}',
        strJSONParams       : '#{cc.attrs.additionalParams}',
        allowMultiSelect    :  #{cc.attrs.allowMultiSelect},
        plup_instance       : null,
        oncompleteCallback  : on_done#{salt},
        onstartCallback     : on_start#{salt}
    };

    if( #{not empty cc.attrs.perFileSuccessCB} )
    {
        cfg_ou#{salt}.perFileSucessCallback = function ( params )
        {
            pf#{salt}(params);
        };
    }

    if( #{not empty cc.attrs.batchSuccessCB} )
    {
        cfg_ou#{salt}.batchSuccessCallback = function ( params )
        {
            pb#{salt}(params);
        };
    }

    function show_ou#{salt}( )
    {
        pfplupload( cfg_ou#{salt} );
    }

    function hide_ou#{salt}()
    {
        s = PrimeFaces.escapeClientId( cfg_ou#{salt}.file_list_id );
        $(s + " > tbody").empty();
        if( cfg_ou#{salt}.plup_instance )
        {
            cfg_ou#{salt}.plup_instance.destroy();
        }
        cfg_ou#{salt}.plup_instance = null;
    }

    function on_done#{salt}()
    {
        plw#{salt}.show();
        hide_ou#{salt}();
        if ( #{not empty cc.attrs.oncomplete} )
        {
            eval("#{cc.attrs.oncomplete}");
        }
        ovl#{salt}.hide();
    }

    function on_start#{salt}()
    {
        if ( #{not empty cc.attrs.onstart} )
        {
            eval("#{cc.attrs.start}");
        }
    }

    function _pf_onSuc#{salt}()
    {
        if ( #{empty cc.attrs.batchUpdate} )
        {
            plw#{salt}.hide();
        }
    }

    // ]]>
        </script>

        <p:remoteCommand name="pf#{salt}"
                         actionListener="#{cc.attrs.perFileSuccessCB}"
                         update="#{not empty cc.attrs.perFileUpdate ? cc.attrs.perFileUpdate : '@none'}"
                         rendered="#{not empty cc.attrs.perFileSuccessCB}"
                         immediate="#{cc.attrs.immediate}"
                         onsuccess="_pf_onSuc#{salt}()"/>

        <p:remoteCommand name="pb#{salt}"
                         actionListener="#{cc.attrs.batchSuccessCB}"
                         update="#{not empty cc.attrs.batchUpdate ? cc.attrs.batchUpdate : '@none'}"
                         rendered="#{not empty cc.attrs.batchSuccessCB}"
                         immediate="#{cc.attrs.immediate}"
                         onsuccess="plw#{salt}.hide()"/>

        <h:panelGroup layout="block" >
            <p:commandButton id="trb" value="#{cc.attrs.triggerLabel}" type="button" rendered="#{cc.attrs.renderAsButton}"  immediate="true" styleClass="#{cc.attrs.buttonStyleClass}"/>
            <p:commandLink id="trl" value="#{cc.attrs.triggerLabel}" rendered="#{!cc.attrs.renderAsButton}"  immediate="true"/>
            <p:overlayPanel id="ou"
                styleClass="#{cc.attrs.panelStyleClass}"
                            for="#{triggerId}"
                            style="width: #{w}px; height: #{h}px;"
                            appendToBody="true"
                            onShow="show_ou#{salt}(this)"
                            onHide="hide_ou#{salt}()"
                            widgetVar="ovl#{salt}">
                <h:panelGroup  id="plP" layout="block" style="height: 100%; position: relative;">
                    <h:panelGroup layout="block" styleClass="block_center" rendered="#{not empty cc.attrs.heading}">
                        <h:outputLabel value="#{cc.attrs.heading}" styleClass="block_center"/>
                    </h:panelGroup>

                    <p:scrollPanel mode="native" style="height: #{hfl}px;" >
                        <table id="#{cc.clientId}:fl" class="pfplupfilelist" >
                            <tbody></tbody>
                        </table>
                    </p:scrollPanel>

                    <h:panelGroup layout="block" style="position: absolute; bottom: 0; width: 100%; text-align: center; " >
                        <p:commandButton id="bf" value="#{cc.attrs.selectBtnLabel}" type="button" styleClass="#{cc.attrs.selectBtnStyleClass}"/>
                        <p:commandButton id="sf"   value="#{cc.attrs.uploadBtnLabel}" type="button" styleClass="#{cc.attrs.uploadBtnStyleClass}"/>
                    </h:panelGroup>
                </h:panelGroup>
            </p:overlayPanel>
        </h:panelGroup>
    </cc:implementation>
</ui:component>

Here is the complementing JavaScript:

//
//A simple JS thing for dealing wth
//PLUPLOAD and JSF and PrimeFaces.
//
//
function pfplupload( cfg )
{
    uploader = new plupload.Uploader({
        container       : cfg.plup_panel_id,
        browse_button   : cfg.browse_button_id,
        runtimes        : 'silverlight,html5,flash',
        url             : cfg.target_url,
        multi_selection : cfg.allowMultiSelect,
        max_file_size   : cfg.maxFileSize,
        filters         : [{
                                title       : cfg.filter_label,
                                extensions  : cfg.filter
                          }],
        flash_swf_url   : cfg.resourcePrefix + '/plupload.flash.swf',
        silverlight_xap_url : cfg.resourcePrefix + '/plupload.silverlight.xap',
        preinit         :
                            {
                                Error: function(up, err)
                                {
                                        if (err.code == -500)
                                        {
                                            s = 'no runtime found; uploading will not work';
                                            if(cfg.noRuntimeMsg)
                                                s = cfg.noRuntimeMsg;
                                            alert(s);
                                        }
                                }
                            }
        });

    cfg.plup_instance = uploader;

    updateButtons = function( filesCount )
    {
        if(!cfg.allowMultiSelect && filesCount > 0 )
        {
            cfg.plup_instance.trigger("DisableBrowse", true);
            $(PrimeFaces.escapeClientId(cfg.browse_button_id)).attr("disabled", true);
            $(PrimeFaces.escapeClientId(cfg.browse_button_id)).addClass('disabled-button');
        }
        else
        {
            cfg.plup_instance.trigger("DisableBrowse", false);
            $(PrimeFaces.escapeClientId(cfg.browse_button_id)).attr("disabled", false);
            $(PrimeFaces.escapeClientId(cfg.browse_button_id)).removeClass('disabled-button');
        }

        if( filesCount > 0 )
        {
            $(PrimeFaces.escapeClientId(cfg.start_button_id)).removeClass('disabled-button');
            $(PrimeFaces.escapeClientId(cfg.start_button_id)).attr("disabled", false);
        }
        else
        {
            $(PrimeFaces.escapeClientId(cfg.start_button_id)).addClass('disabled-button');
            $(PrimeFaces.escapeClientId(cfg.start_button_id)).attr("disabled", true);
        }
    };

    uploader.bind('Error', function(up, err)
    {
        if(err.file)
        {
            var comp_id = PrimeFaces.escapeClientId(cfg.file_list_id);
            var parent_selector = comp_id+' > tbody';
            var row_selector = '#'+err.file.id;
            var errMessage = "";
            if (err.message)
            {
                errMessage += "Message: "+ err.message+" ";
            }
            if(err.details)
            {
                errMessage += "Details: "+err.details+" ";
            }
            if(err.code)
            {
                errMessage += "Code: "+err.code;
            }
            var row =
            "<tr id='" + err.file.id +"'>" +
            "<td>" + err.file.name + " (" + plupload.formatSize(err.file.size) + ")</td>" +
            "<td id='" + err.file.id + "_i'>" +
            "<a href='javascript:alert(\""+errMessage+"\")'>" +
            "<img src='" + cfg.imagePrefix + "/pfplup-error.gif'/>" +
            "</a>" +
            "</td>" +
            "</tr>";
            var rowSelected = $(row_selector);
            if(rowSelected.length === 0)
            {
                $(parent_selector).append(row);
            }
            else
            {
                rowSelected.replaceWith(row);
            }
        }
        else
        {
            if(err.code != -500)
                alert("Unknown upload error:  " + err.code + "; message: " + err.message);
        }
    });

    uploader.bind('FilesAdded', function(up, files)
    {
        comp_id = PrimeFaces.escapeClientId(cfg.file_list_id);
        $.each(files, function(i, file)
        {
            var row_selector = '#'+file.id;
            var rowSelected = $(row_selector);
            if(rowSelected.length === 0){
                row =
                "<tr id='" + file.id +"'>" +
                "<td>" + file.name + " (" + plupload.formatSize(file.size) + ")</td>" +
                "<td id='" + file.id + "_i'>" +
                "<img id='" + file.id + "_d' src='" + cfg.imagePrefix + "/pfplup-subtract.gif'/>" +
                "</td>" +
                "</tr>";

                $(comp_id + " > tbody").append(row);

                $('#' + file.id + "_d").live('click', function()
                {
                    f = up.getFile(file.id);
                    if(f)
                    {
                        up.removeFile(f);
                        r = $('#' + file.id);
                        r.remove();
                        up.refresh();
                        updateButtons( up.files.length );
                    }
                });
            }//end if rowSelected.length
        });

        updateButtons( files.length );
        up.refresh();
    });

    uploader.bind('BeforeUpload', function(up, file)
    {
        up.settings.url = cfg.target_url + '?id=' + file.id;
        $("td#" + file.id +"_i").html("<img src='" + cfg.imagePrefix + "/pfplup-uploading.gif'/>");
    });

    uploader.bind('FileUploaded', function(up, file, resp)
    {
        obj = $.parseJSON(resp.response);
        if(file.status == plupload.FAILED || obj.status == 'fail')
        {
            up.trigger('Error', {   code : plupload.HTTP_ERROR,
                                    message : plupload.translate('HTTP Error.'),
                                    file : file,
                                    status : plupload.FAILED});
            up.stop();
            return false;
        }
        slctr='td#' + file.id + '_i';
        $(slctr).html("<img src='" + cfg.imagePrefix + "/pfplup-processing.gif'/>");
        params =
        [
            {
                name    : 'file_name',
                value   : file.name
            },

            {
                name    : 'temp_file_name',
                value   : file.target_name
            },

            {
                name    : 'file_guid',
                value   : file.id
            }
        ];
        if(cfg.perFileSucessCallback)
        {
            if(cfg.strJSONParams)
            {
                p = eval(cfg.strJSONParams);
                params = params.concat(p);
            }
            cfg.perFileSucessCallback(params);
        }
        $(slctr).html("<img src='" + cfg.imagePrefix + "/pfplup-done.gif'/>");
    });

    uploader.bind('UploadComplete', function(up, files)
    {
        if(cfg.batchSuccessCallback)
        {
            cfg.batchSuccessCallback();
        }
        if(cfg.oncompleteCallback)
        {
            cfg.oncompleteCallback();
        }
    });

    uploader.init();

    $(PrimeFaces.escapeClientId(cfg.start_button_id)).on("click", function()
    {
        if(cfg.onstartCallback)
        {
            cfg.onstartCallback();
        }
        uploader.start();
    });

    updateButtons( 0 );
}

Finally, here is the servlet 'catch file' code:

package fhw;
import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
/**
 * Catches files launched at us by PLUPLOAD flash or silverlight or others widget.
 *
 * Inspired & Copied from http://blog.shadit.com/2010/10/28/java-servlet-plupload/
 */
@WebServlet(urlPatterns = "/u",
            initParams =
{
    @WebInitParam(name = "uploadDirectory", value = "/tmp"),
    @WebInitParam(name = "bufferSize", value = "32768")
})
public class PluploadCatchServlet
        extends HttpServlet
{
    static private final String RESP_SUCCESS = "{\"jsonrpc\" : \"2.0\", \"result\" : \"success\", \"id\" : \"id\"}";
    static private final String RESP_ERROR = "{\"jsonrpc\" : \"2.0\", \"error\" : {\"code\": 101, \"message\": \"Failed to open input stream.\"}, \"id\" : \"id\"}";
    static public final String SEP = System.getProperty("file.separator");
    static public final String JSON = "application/json";
    private final static Logger logger = Logger.getLogger(PluploadCatchServlet.class.getName());
    private static final long serialVersionUID = 6861348980737171106L;

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
    {
        String responseString = RESP_SUCCESS;
        boolean isMultipart = ServletFileUpload.isMultipartContent(req);

        if (isMultipart)
        {
            ServletFileUpload upload = new ServletFileUpload();
            try
            {
                String id = req.getParameter("id");
                FileItemIterator iter = upload.getItemIterator(req);
                while (iter.hasNext())
                {
                    FileItemStream item = iter.next();
                    InputStream input = item.openStream();
                    if (!item.isFormField())
                    {
                        if(StringUtils.isEmptyString(id))
                        {
                            throw new ServletException("id request parameter missing during plupload operation");
                        }
                        saveUploadFile(input, item, id);
                    }
                }
            }
            catch (Exception e)
            {
                responseString = RESP_ERROR;
                logger.log(Level.WARNING, "Failed to catch a file from PLUPLOADER", e);
            }
        }
        else
        {
            responseString = RESP_ERROR;
        }
        resp.setContentType(JSON);
        byte[] responseBytes = responseString.getBytes();
        resp.setContentLength(responseBytes.length);
        ServletOutputStream output = resp.getOutputStream();
        output.write(responseBytes);
        output.flush();
    }

    private void saveUploadFile(InputStream input, FileItemStream item, String id)
        throws IOException
    {
        String s = getServletContext().getInitParameter("bufferSize");
        //
        //See if 'global' (web.xml) bufferSize is set first....
        //
        if(StringUtils.isEmptyString(s))
        {
            s = getInitParameter("bufferSize");
        }
        int buffSz = Integer.parseInt(s);
        String uplD= getServletContext().getInitParameter("uploadDirectory");
        //
        //See if 'global' (web.xml) uploadDir is set first....
        //
        if(StringUtils.isEmptyString(uplD))
        {
            uplD = getInitParameter("uploadDirectory");
        }
        File localFile = new File(uplD + SEP + id);
        System.out.println("I AM SAVING TO:   " + localFile.getAbsolutePath());
        System.out.println("getFieldName:   " + item.getFieldName());
        System.out.println("getName:   " + item.getName());
        BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(localFile));
        byte[] data = new byte[buffSz];
        int count;
        while ((count = input.read(data, 0, buffSz)) != -1)
        {
            output.write(data, 0, count);
        }
        input.close();
        output.flush();
        output.close();
    }
    
    private static class StringUtils
    {
        /**
         * Returns true if the value specified is either NULL or empty string ("")
         * or string full of whitespaces. (This does not create new strings during
         * emptiness evaluation).
         *
         * @param inputStr The string to check for emptiness.
         * @return true if the string is empty; false otherwise.
         */
        public static boolean isEmptyString(String inputStr)
        {
            boolean ret = true;
            if (inputStr != null)
            {
                int len = inputStr.length();
                for(int lll = 0; lll < len; lll++)
                {
                    if (!Character.isWhitespace(inputStr.charAt(lll)))
                    {
                        ret = false;
                        break;
                    }
                }
            }
            return (ret);
        }
    }    
}


Here is a link to the full Netbean 7.2 project with a working sample.  To use, you'll need a copy of PrimeFaces (this project used 3.4.1, and commons io (2.4) and commons upload 1.2.2.   I tested this with Glassfish 3.1.2.2.

Here are a few closing notes & thoughts on this implementation
  • plupload doesn't like to be 'updated'. In my real project we use lots of ajax and partial page rendering. Re-rendering a part of a page that 'contains' the pluploadoverlay, would cause all plupload instances on the page to stop working. I had to do a considerable amount of tinkering on our facelets with this in mind.
  • In my real application, the actual file uploading part goes pretty quick; but my backing bean processing is sorta slow. Since I auto-closed the overlay on complete, it would disappear quickly, even if the underlying p:remote/ajax call was not really finished. This made for an awkward user experience and especially problematic if there were several files to be uploaded, as the update targets were not refreshed until p:remote/ajax we indeed finished. I added in a simple modal 'please wait' device that was shown and hidden while the ajax calls were completing.
  • The CC has lots of life-cycle call backs and update targets for specific events - I think this could be simpler; but when I initially built this, having all the hooks seemed like a good thing.
  • The overlay is still a problem, as it will disappear on any mouse click not inside the overlay region. I'd really like this to be modal like with the auto close and a 'cancel' device. In the near future, I will probably refactor away from overlay and towards a dialog or lightbox style presentation.

3 comments:

  1. sorry went on vaca and refactored it some -- I actually have 2 diff approaches cooking now. Will post some updates shortly....

    ReplyDelete