My JSF Books/Videos My JSF Tutorials OmniFaces/JSF PPTs
JSF 2.3 Tutorial
JSF Caching Tutorial
JSF Navigation Tutorial
JSF Scopes Tutorial
JSF Page Author Beginner's Guide
OmniFaces 2.3 Tutorial Examples
OmniFaces 2.2 Tutorial Examples
JSF Events Tutorial
OmniFaces Callbacks Usages
JSF State Tutorial
JSF and Design Patterns
JSF 2.3 New Features (2.3-m04)
Introduction to OmniFaces
25+ Reasons to use OmniFaces in JSF
OmniFaces Validators
OmniFaces Converters
JSF Design Patterns
Mastering OmniFaces
Reusable and less-verbose JSF code

My JSF Resources ...

Java EE Guardian
Member of JCG Program
Member MVB DZone
Blog curated on ZEEF
OmniFaces is an utility library for JSF, including PrimeFaces, RichFaces, ICEfaces ...

.

.

.

.

.

.

.

.


[OmniFaces Utilities] - Find the right JSF OmniFaces 2 utilities methods/functions

Search on blog

Petition by Java EE Guardians

Twitter

duminică, 1 februarie 2015

JSF 2.2 Multiple File Upload with HTML 5 and AJAX

This post was updated here:

JSF 2.3 Multiple File Upload with HTML 5, AJAX and upload progress bar via web sockets


In this post, we try to achieve a multiple upload for images, like in figure below (this was tested on Mozilla Firefox and Google Chrome):

        initial view
        some file selected
        after upload

Starting with JSF 2.2, we can use the upload facility via a new built-in component, named HtmlInputFile. This component is available for page authors as <h:inputFile> tag. Until JSF 2.2, this was possible only with JSF extensions, like PrimeFaces or RichFaces.
Basically, this component renders an HTML 5 input element of type file, and it is based on Servlet 3.0, which is part of Java EE since version 6. Servlet 3.0 provides an upload mechanism based on the javax.servlet.http.Part interface and the @MultipartConfig annotation. If you take a quick look over the JSF 2.2 FacesServlet source code, you will notice that it was annotated with @MultipartConfig especially for handling multipart data.
By default, the JSF 2.2 implementation allows to select a single file for upload per input file component. This means that  JSF 2.2 does not provide support for uploading multiple files, but, with some adjustments, we can achieve this goal. In order to have multiple file uploads, you need to focus on two aspects, which are listed as follows:
        Making multiple file selections possible
        Uploading all the selected files
So, first, let's take a basic usage of this component, and, afterwards, we will add some code for achieving our goal:

<h:form id="uploadFormId" enctype="multipart/form-data">
 ...
 <h:inputFile id="fileToUploadId" title="Select Files" value="#{uploadBean.files}"/>
 ...
</h:form> 

This snippet allows a single file to be selected by the user. The multiple selection can be activated using an HTML5 input file attribute named, multiple and the JSF 2.2 pass-through attribute feature. When this attribute is present and its value is set to multiple, the user can select multiple files. So, this task requires some minimal adjustments:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f5="http://xmlns.jcp.org/jsf/passthrough"
      xmlns:f="http://xmlns.jcp.org/jsf/core">
 ...
 <h:form id="uploadFormId" enctype="multipart/form-data">
  ...
  <h:inputFile id="fileToUploadId" f5:multiple="multiple"
     title="Select Files" value="#{uploadBean.files}"/>
  ...
 </h:form> 

Well, this was simple! But, even if we can select multiple files, it doesn't mean that we will upload all the selected files. For accomplishing this, we need to go further and check the HtmlInputFile renderer. JSF will override the previous Part instance with each file in the uploaded set. This is normal, since, on the server-side, you use an object of type Part, while you need a collection of Part instances. Fixing this issue requires us to focus on the renderer of the file component. This renderer is named FileRenderer (an extension of TextRenderer), and the decode() method implementation is the key for our issue (the highlighted code is very important for us), as shown in the following code:

public class FileRenderer extends TextRenderer {

 @Override
 public void decode(FacesContext context, UIComponent component) {

  rendererParamsNotNull(context, component);

  if (!shouldDecode(component)) {
      return;
  }

  String clientId = decodeBehaviors(context, component);

  if (clientId == null) {
      clientId = component.getClientId(context);
  }

  assert(clientId != null);
  ExternalContext externalContext = context.getExternalContext();
  Map<String, String> requestMap = externalContext.getRequestParameterMap();
       
  if (requestMap.containsKey(clientId)) {
      setSubmittedValue(component, requestMap.get(clientId));
  }

  HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
  try {
      Collection<Part> parts = request.getParts();
      for (Part cur : parts) {
           if (clientId.equals(cur.getName())) {
               component.setTransient(true);
               setSubmittedValue(component, cur);
           }
      }
  } catch (IOException ioe) {
    throw new FacesException(ioe);
  } catch (ServletException se) {
    throw new FacesException(se);
  }
           
 }
 ...
   
The highlighted code causes the override Part issue, but you can easily modify it to submit a list of Part instances instead of one Part, as follows:

public class MultipleFileRenderer extends FileRenderer {
 ...
 try {
     Collection<Part> parts = request.getParts();
     List<Part> multiple = new ArrayList<>();
     for (Part cur : parts) {
          if (clientId.equals(cur.getName())) {
              component.setTransient(true);
              multiple.add(cur);
          }
     }
     this.setSubmittedValue(component, multiple);
 } catch (IOException | ServletException ioe) {
   throw new FacesException(ioe);
 }
 ...

Of course, in order to modify this code, you need to create a custom file renderer and configure it properly in faces-config.xml:

<render-kit>
  <renderer>
   <component-family>javax.faces.Input</component-family>
   <renderer-type>javax.faces.File</renderer-type>
   <renderer-class>package.name.MultipleFileRenderer</renderer-class>
  </renderer>
 </render-kit>

Afterwards, you can define a list of Part instances in your managed bean using the following code:

...
private List<Part> files;

 public List<Part> getFile() {
  return files;
 }

 public void setFile(List<Part> files) {
  this.files = files;
 }
 ...

Each entry in the list is a file; therefore, you can write them on the disk by iterating the list using the following code:

 ...
 for (Part file : files) {
      ...

Well, at this point we can select and upload multiple files. Further, we want to provide a quick preview (thumb) for the selected images. This can be easily accomplished via the HTML 5, File, FileList and FileReader APIs. First, we need to know when the user selects something, so we can use the addEventListener() in a pure JavaScript style:

...
document.getElementById('uploadFormId:fileToUploadId').addEventListener('change', handleFileSelect, false);
...

Now, each time the user makes a selection the handleFileSelect() method is called. Here, we can extract the value of the input file component, which is a FileList. Next, we can loop this list and inspect each file.  So, here we can apply some client side validation, like accepting only images smaller than 2 MB. The accepted files are read further via FileReader.readAsDataURL() function:

var OK = "";
var WRONG_TYPE = "You can upload only images files !";
var WRONG_SIZE = "You can upload only images smaller than 2 MB !";
...
function handleFileSelect(evt) {

 var MAX_FILES = 5;
 var MAX_MB = 2097152; //2 MB              
 var WRONG_NUMBER = "You can select maxim 5 images !";
 ...
 document.getElementById("uploadFormId:uploadMessagesId").innerHTML = "";
 document.getElementById("fatalErrorsId").innerHTML = "";
 document.getElementById('thumbnails').innerHTML = "";

 evt.stopPropagation();
 evt.preventDefault();

 var files = evt.target.files;

 if (files.length > MAX_FILES) {
     document.getElementById("fatalErrorsId").innerHTML = ['', WRONG_NUMBER, ''].join('');
 } else {
     for (var i = 0; i < files.length; i++) {

          var f = files[i];

          // only process image files
          if (!f.type.match('image.*')) {
              addFileToThumbnails(f, WRONG_TYPE);
              continue;
          }

          // only files smaller than 2 MB
          if (f.size > MAX_MB) {
              addFileToThumbnails(f, WRONG_SIZE);
              continue;
          }
 
         //optional - you may add here file name length validation !
 
         var reader = new FileReader();

         // closure to capture the file information
         reader.onload = (function (theFile) {
          return function (e) {
           addFileToThumbnails(theFile, OK, e.target.result);
          };
         })(f);

         // read in the image file as a data URL
         reader.readAsDataURL(f);
    }
 }
}

The IDs, uploadFormId:uploadMessagesId,  fatalErrorsId and thumbnails can be identified below:

<h:form id="uploadFormId" enctype="multipart/form-data">
  ...
  <h:inputFile id="fileToUploadId" f5:multiple="multiple"
     title="Select Files" value="#{uploadBean.files}"/>
  <table id="thumbnails" border="0"></table>
  ...

  <h:messages id="uploadMessagesId" showDetail="false" showSummary="true"
              for="fileToUploadId"  infoClass="success" errorClass="error"/>
 </h:form> 
 <div id="fatalErrorsId" class="error"></div>

So, for each image, we call a function named addFileToThumbnails() with different parameters. In this function, for accepted files, we write the code for generating a simple <img> thumb and to provide some info about the image, like name and size - we put these in a simple <table>, but you can use an <ul>, or something else. For non-valid images, we just generate a nice message (see sample below):


The cellThumb, cellName, cellErr and cellTrash are filled up in the addFileToThumbnails() below:

function addFileToThumbnails(theFile, errCode, filePath) {

 var KB = 1024;
 var MB = 1048576; //1 MB
 var MAX_LENGTH = 30; //characters

 // render thumbnail
 var fileName = theFile.name;
 if (theFile.name.length > MAX_LENGTH) {
     fileName = theFile.name.substring(0, MAX_LENGTH) + "...";
 }
 var fileSize = 0;
 if (theFile.size > MB)
     fileSize = (Math.round(theFile.size * 100 / (MB)) / 100).toString() + 'MB';
 else
     fileSize = (Math.round(theFile.size * 100 / KB) / 100).toString() + 'KB';

 // populate the thumbnails table
 var thumbnails = document.getElementById("thumbnails");
 var row = thumbnails.insertRow(-1);

 // add a thumb for an image
 if (errCode === OK) {
     var cellThumb = row.insertCell(0);
     var cellName = row.insertCell(1);
     var cellTrash = row.insertCell(2);
     cellThumb.style.width = '60px';
     cellThumb.innerHTML = ['<div class="grow"><img class="thumb" src="', filePath,
                           '" title="', escape(theFile.name), '"/></div>'].join('');
     cellName.style.width = '460px';
     cellName.innerHTML = ['<span class="files-info">', fileName, '</span><br/>\n\
                           <span class="files-size">', fileSize, '<span>'].join('');
     cellTrash.style.width = '20px';
     cellTrash.innerHTML = ['<div class="bw"><img src="./resources/ajax/btns/trash.png"\n\
                           onclick="sendFileToTrash(', row.rowIndex, ',\'', theFile.name, '\');"
                           title=""/></div>'].join('');
 }

 // a thumb for a non-valid image
if (errCode === WRONG_TYPE || errCode === WRONG_SIZE) {
    var cellErr = row.insertCell(0);
    cellErr.colSpan = "3";
    cellErr.style.width = '540px';
    cellErr.innerHTML = ['<div class="err-div"><img src="./resources/ajax/btns/err.png"/>\n\
                        <span class="files-info">', fileName, ' (', fileSize, ')</span><br/>',
                        errCode, '</div>'].join('');
    }
}

When a file is not accepted, or the user changes his mind and reject a file by pressing the "X" icon (or press the Cancel button, which reject all selected files), then that file should be removed from the FileList. Well, this is very easy to say, and very hard to do, because the FileList object is a read only list, so it is not possible (and, obviously, from security reasons, not recommended), to modify its content. ONLY the user selection should modify its content. This is an important drawback, because we cannot actually remove the unnecessary files from being uploaded. Nevertheless, is seams that we can empty the list completely, like this:

document.getElementById('uploadFormId:fileToUploadId').value = "";

So, we can easily distinguish several approaches here:
        when at least one file is invalid, we cancel the entire upload and fire a corresponding message (this was implemented in this case and was named: high validation level - validateLevel('high');). Of course, if the user changes his mind and reject some files by clicking the "X", we cannot consider them as invalid, so the entire list of selected files will be send. But, we can send a list that contains the names of the rejected files also, and, on server side, write on disk only the necessary files. Of course, we can also suppress the feature of rejecting files after selection.
        we always  send to server all selected files, and a list with the rejected files names (invalid or deselected by the user), and write on disk only the necessary files (this was implemented in this case and was named: low validation level - validateLevel('low');)
        copy the FileList content into a JavaScript array, which can be the source for a FormData object created from scratch - here, we can keep only the necessary files. Further, we can use a pure AJAX (based on XMLHttpRequest) to send the FormData content. Or, even simpler, loop the FileList and send via AJAX only valid/not rejected files. But, you have to find a way to call a JSF managed bean method, or use a separate Servlet. Not implemented in this case, because it practically bypasses JSF, so is kind of "strange" approach.
        write a custom upload component requires solid "underground" knowledge and the time consumed will not be justified. Not implemented in this case!

So, we are adding the not valid/rejected files names into an JavaScript object:

trash = {items: []};

Further, at submit, we place this in a <h:inputHidden>:

trash.items.push({name: fileName});
document.getElementById('uploadFormId:trashId').value = JSON.stringify(trash);

And, the hidden field in place in our form:

<h:form ...>
 <h:inputHidden id="trashId" value="#{uploadBean.trash}"/>
</h:form>

Now, obviously, on server side, we can find out what files should be written on disk. When the number of selected files is equal to the number of rejected/not valid files, then we don't fire the submit request, and just reset the input file value, and display a message.

Next, we can focus on the Upload button:

<h:form ...>
  <h:commandButton id="uploadBtnId" image="#{resource['ajax:btns/uploadbtn.png']}"
                   actionListener="#{uploadBean.upload}" onclick="return validateLevel('high');"
                   styleClass="upload-btn">
  <f:ajax execute="fileToUploadId trashId" render="uploadMessagesId"
          onevent="uploadProgress" onerror="uploadError"/>
  ...
  </h:commandButton>
</h:form>

The thing important here is the way we signal to the user the upload progress. Well, everybody want to see a determinate progress bar (like the one used by the PrimeFaces FileUpload component), but this is not so simple to achieve via pure JSF - trying to provide a proxy for JSF AJAX library will be an approach. The problem lies in the JSF AJAX mechanism which is based on a hidden iframe for transport. This means that we cannot work with HTML 5 Progress Events API. So, this is not possible:

...
var xhr = JSF_XMLHttpRequest;

xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.addEventListener("load", uploadComplete, false);
xhr.addEventListener("error", uploadFailed, false);
xhr.addEventListener("abort", uploadCanceled, false);
...

In JSF 2.2, FacesServlet was annotated with @MultipartConfig for dealing multipart data (upload files), but there is no progress listener interface for it. Moreover, FacesServlet is declared final; therefore, we cannot extend it. Well, the possible approaches are pretty limited by these aspects. In order to
implement a server-side progress bar, we need to implement the upload process in a separate class (Servlet) and provide a listener. Or, we have to be satisfied with an indeterminate progress bar, like in figure below:


This can be controlled via onevent attribute which calls a JavaScript method, named uploadProgress():

function uploadProgress(data) {
 if (data.status === "begin") {
     start();
 }
 if (data.status === "complete") {
     stop();
 }
}

For brevity, start() and stop() are not listed here. Basically, they just show/hide the indeterminate progress bar via pure JavaScript code.

Note The Cancel button from the above image is capable to "clean" the current selection, thumbs, messages, etc by calling a JavaScript method named, sendAllFilesToTrash(). For brevity, it is not listed here.

Some uploads provide an Abort Upload/Cancel Upload button (not implemented here), which is capable to force the AJAX request (upload) to stop during its execution. Well, the JSF AJAX requests are asynchronous, so we can execute more JavaScript code, even more AJAX requests, but,  NOT more JSF AJAX requests, because, by default, JSF put the JSF AJAX requests in a queue and fires an AJAX request only after the preceding one is complete. The queue is managed by JSF, and this behavior maintains the integrity and thread safety of the JSF view state. So, apparently, JSF AJAX requests may look as if they are not asynchronous. This is why we cannot fire an abort/cancel JSF AJAX request to a method of an managed bean capable to stop the upload. On client side, a JSF AJAX request can be suddenly aborted if we stop the hidden iframe, like this:

window.frames[0].stop();

But, there are at least two drawbacks here:
        the JSF AJAX library should be re-initialized
        the server-side will cause a "brutal" error

As a final touches on the client-side, we can:
        limit the files types listed on selection by using the HTML 5, accept attribute

<h:inputFile id="fileToUploadId" f5:accept="image/*" f5:multiple="multiple"
             title="Select Files" value="#{uploadBean.files}"/>

        provide a fix and custom design for the Select Files button (you can see the CSS code in the complete code) - normally, this button looks different depending on browser

...
<div class="file-upload">
 <h:outputLabel for="fileToUploadId" styleClass="file-upload-span" value="SELECT FILES"/>
 <h:inputFile id="fileToUploadId" f5:accept="image/*" f5:multiple="multiple"
              title="Select Files" value="#{uploadBean.files}"/>
</div>
...

        on thumb hover, provide a "grow" effect by using some CSS effects


Now, we can see the server-side code, which is pretty simple. First, we pass the validation rules via <f:attribute>. You can use <f:param>, <h:inputHidden>, hardcode them on server-side etc:
...
<h:commandButton id="uploadBtnId" image="#{resource['ajax:btns/uploadbtn.png']}"
                 actionListener="#{uploadBean.upload}" onclick="return validateLevel('high');"  
                 styleClass="upload-btn">
  <f:ajax execute="fileToUploadId trashId" render="uploadMessagesId"
          onevent="uploadProgress" onerror="uploadError"/>
  <f:attribute name="maxFilesNumber" value="5"/>
  <f:attribute name="maxFileSize" value="2097152"/>
  <f:attribute name="fileTypes" value="image/"/>
 </h:commandButton>
...

Note The server-side validation is accomplished in the managed bean, not in a custom JSF validator. If you choose the high validation level - validateLevel('high');, then you can write a. custom validator, and throw a ValidatorException when the first invalid file is found. This will cancel the upload, which is ok. But, if you are using the low validation level - validateLevel('low');, then the validator cannot "sift" the invalid files and keep the valid ones. The validator cannot alter the validated value. Nevertheless, you can use a custom validator, and FacesContext.getAttributes() to isolate the invalid files. By placing the validation in the managed bean, we easily cover both cases. 

Check the code:

//imports here
...
@Named
@RequestScoped
public class UploadBean {

 private static final Logger logger = Logger.getLogger(UploadBean.class.getName());
 private List<Part> files;
 private String trash = "{items: []}";

 public void upload(ActionEvent event) {              

  if (files != null) {

      int countFiles = 0;
      String trashFiles = trash.substring(trash.indexOf(":"), trash.length() - 1);
      byte maxFilesNumber = Byte.parseByte((String)
       event.getComponent().getAttributes().get("maxFilesNumber"));
      int maxFileSize = Integer.parseInt((String)
       event.getComponent().getAttributes().get("maxFileSize"));
      String fileTypes = (String) event.getComponent().getAttributes().get("fileTypes");

      logger.log(Level.INFO, "Files trash:{0}", trash);

      logger.info("Files Details:");
      for (Part file : files) {

       // validate the file name
       String fileName = file.getSubmittedFileName().trim();
       if (!fileName.isEmpty()) {

            String fileNameToDisplay = (fileName.length() > 20) ? fileName.substring(0, 17)+" ..." :  fileName;

           // check if this is trash file
           if (!trashFiles.contains("\"name\":\"" + fileName + "\"")) {

               //validate content type
               if (file.getContentType().startsWith(fileTypes)) {

                   // validate file size                               
                   if (file.getSize() <= maxFileSize) {

                       // validate maximum number of files
                       if (countFiles < maxFilesNumber) {

                           logger.log(Level.INFO, "File component id:{0}", file.getName());
                           logger.log(Level.INFO, "Content type:{0}", file.getContentType());
                           logger.log(Level.INFO, "Submitted file name:{0}", file.getSubmittedFileName());
                           logger.log(Level.INFO, "File size:{0}", file.getSize());

                           // D:/files - just a dummy path on my machine
                           try (InputStream inputStream = file.getInputStream(); 
                                FileOutputStream outputStream = new FileOutputStream(
                                "D:" + File.separator + "files" + File.separator + file.getSubmittedFileName())) {

                                int bytesRead = 0;
                                final byte[] chunck = new byte[1024];
                                while ((bytesRead = inputStream.read(chunck)) != -1) {
                                        outputStream.write(chunck, 0, bytesRead);
                                }

                                countFiles++;                                                                        
                                FacesContext.getCurrentInstance().addMessage
                                ("uploadFormId:fileToUploadId", new
                                FacesMessage(FacesMessage.SEVERITY_INFO, "Upload successfully
                                ended: " + fileNameToDisplay, ""));
                           } catch (IOException ex) {
                             FacesContext.getCurrentInstance().addMessage
                              ("uploadFormId:fileToUploadId", new
                              FacesMessage(FacesMessage.SEVERITY_ERROR, "Upload " +
                              fileNameToDisplay + " failed !", ""));
                           }
                       } else {                                   
                         FacesContext.getCurrentInstance().addMessage
                         ("uploadFormId:fileToUploadId", new   
                         FacesMessage(FacesMessage.SEVERITY_ERROR, "You can upload maxim 5 images !", ""));
                         break;
                       }
                    } else {                              
                     FacesContext.getCurrentInstance().addMessage("uploadFormId:fileToUploadId",
                     new FacesMessage(FacesMessage.SEVERITY_ERROR, "File: " +
                     fileNameToDisplay + " has more than 2 MB !", ""));
                   }
               } else {                          
                 FacesContext.getCurrentInstance().addMessage("uploadFormId:fileToUploadId", new
                 FacesMessage(FacesMessage.SEVERITY_ERROR, "File " + fileNameToDisplay + " is not an accepted image !", ""));
               }
           }
       }
      }
    
      if (countFiles == 0) {
          FacesContext.getCurrentInstance().addMessage("uploadFormId:fileToUploadId", new
            FacesMessage(FacesMessage.SEVERITY_ERROR, "There are no files to upload !", ""));
      }
  }
 }

 public String getTrash() {
  return trash;
 }

 public void setTrash(String trash) {
  this.trash = trash;
 }

 public List<Part> getFiles() {
  return files;
 }
 public void setFiles(List<Part> files) {
  this.files = files;
 }
}

In order to test the server-side validation, you need to suppress the client-side validation. In figure below, you can see a messages that comes from the server-side validation:


Done! You can find the complete code here (JSFMultipleFileUpload).

Niciun comentariu :

Trimiteți un comentariu

JSF BOOKS COLLECTION

Postări populare

OmniFaces/JSF Fans

Visitors Starting 4 September 2015

Locations of Site Visitors