Uploading multiple files

Master the art of implementing user interfaces with JSF 2.2

(For more resources related to this topic, see here.)

Regarding the first task, the multiple selection can be activated using an HTML5 input file attribute (multiple) and the JSF 2.2 pass-through attribute feature. When this attribute is present and its value is set to multiple, the file chooser 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"> ... <h:form id="uploadFormId" enctype="multipart/form-data"> <h:inputFile id="fileToUpload" required="true" f5:multiple="multiple" requiredMessage="No file selected ..." value="#{uploadBean.file}"/> <h:commandButton value="Upload" action="#{uploadBean.upload()}"/> </h:form>

The second task is a little bit tricky, because when multiple files are selected, JSF will overwrite the previous Part instance with each file in the uploaded set. This is normal, since you use an object of type Part, but 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 bold code is very important for us), as shown in the following code:

@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:

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.

Afterwards, you can define a list of Part instances in your 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) { try (InputStream inputStream = file.getInputStream(); FileOutputStream outputStream = new FileOutputStream("D:" + File.separator + "files"
+ File.separator + getSubmittedFileName())) { int bytesRead = 0; final byte[] chunck = new byte[1024]; while ((bytesRead = inputStream.read(chunck)) != -1) { outputStream.write(chunck, 0, bytesRead); } FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload successfully ended: " + file.getSubmittedFileName())); } catch (IOException e) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("Upload failed !")); } } ...

Upload and the indeterminate progress bar

When users upload small files, the process happens pretty fast; however, when large files are involved, it may take several seconds, or even minutes, to end. In this case, it is a good practice to implement a progress bar that indicates the upload status. The simplest progress bar is known as an indeterminate progress bar, because it shows that the process is running, but it doesn't provide information for estimating the time left or the amount of processed bytes.

In order to implement a progress bar, you need to develop an AJAX-based upload. The JSF AJAX mechanism allows us to determine when the AJAX request begins and when it completes. This can be achieved on the client side; therefore, an indeterminate progress bar can be easily implemented using the following code:

<script type="text/javascript"> function progressBar(data) { if (data.status === "begin") { document.getElementById("uploadMsgId").innerHTML=""; document.getElementById("progressBarId"). setAttribute("src", "./resources/progress_bar.gif"); } if (data.status === "complete") { document.getElementById("progressBarId").removeAttribute("src"); } } </script> ... <h:body> <h:messages id="uploadMsgId" globalOnly="true" showDetail="false" showSummary="true" style="color:red"/> <h:form id="uploadFormId" enctype="multipart/form-data"> <h:inputFile id="fileToUpload" required="true" requiredMessage="No file selected ..." value="#{uploadBean.file}"/> <h:message showDetail="false" showSummary="true" for="fileToUpload" style="color:red"/> <h:commandButton value="Upload" action="#{uploadBean.upload()}"> <f:ajax execute="fileToUpload" onevent="progressBar" render=":uploadMsgId @form"/> </h:commandButton> </h:form> <div> <img id="progressBarId" width="250px;" height="23"/> </div> </h:body>

A possible output is as follows:

Upload and the determinate progress bar

A determinate progress bar is much more complicated. Usually, such a progress bar is based on a listener capable to monitor the transferred bytes (if you have worked with Apache Commons' FileUpload, you must have had the chance to implement such a listener). 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 component in a separate class (servlet) and provide a listener. Alternatively, on the client side, we need a custom POST request that tricks FacesServlet that the request is formatted by jsf.js.

In this article, you will see a workaround based on HTML5 XMLHttpRequest Level 2 (can upload/download streams as Blob, File, and FormData), HTML5 progress events (for upload it returns total transferred bytes and uploaded bytes), HTML5 progress bar, and a custom Servlet 3.0. If you are not familiar with these HTML5 features, then you have to check out some dedicated documentation.

After you get familiar with these HTML5 features, it will be very easy to understand the following client-side code. First we have the following JavaScript code:

<script type="text/javascript"> function fileSelected() { hideProgressBar(); updateProgress(0); document.getElementById("uploadStatus").innerHTML = ""; var file = document.getElementById('fileToUploadForm: fileToUpload').files[0]; if (file) { var fileSize = 0; if (file.size > 1048576) fileSize = (Math.round(file.size * 100 / (1048576)) / 100).toString() + 'MB'; else fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; document.getElementById('fileName').innerHTML = 'Name: ' + file.name; document.getElementById('fileSize').innerHTML = 'Size: ' + fileSize; document.getElementById('fileType').innerHTML = 'Type: ' + file.type; } } function uploadFile() { showProgressBar(); var fd = new FormData(); fd.append("fileToUpload", document.getElementById('fileToUploadForm: fileToUpload').files[0]); var xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", uploadProgress, false); xhr.addEventListener("load", uploadComplete, false); xhr.addEventListener("error", uploadFailed, false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open("POST", "UploadServlet"); xhr.send(fd); } function uploadProgress(evt) { if (evt.lengthComputable) { var percentComplete = Math.round(evt.loaded * 100 / evt.total); updateProgress(percentComplete); } } function uploadComplete(evt) { document.getElementById("uploadStatus").innerHTML = "Upload successfully completed!"; } function uploadFailed(evt) { hideProgressBar(); document.getElementById("uploadStatus").innerHTML = "The upload cannot
be complete!"; } function uploadCanceled(evt) { hideProgressBar(); document.getElementById("uploadStatus").innerHTML = "The upload was canceled!"; } var updateProgress = function(value) { var pBar = document.getElementById("progressBar"); document.getElementById("progressNumber").innerHTML=value+"%"; pBar.value = value; } function hideProgressBar() { document.getElementById("progressBar").style.visibility = "hidden"; document.getElementById("progressNumber").style.visibility = "hidden"; } function showProgressBar() { document.getElementById("progressBar").style.visibility = "visible"; document.getElementById("progressNumber").style.visibility = "visible"; } </script>

Further, we have the upload component that uses the preceding JavaScript code:

<h:body> <hr/> <div id="fileName"></div> <div id="fileSize"></div> <div id="fileType"></div> <hr/> <h:form id="fileToUploadForm" enctype="multipart/form-data"> <h:inputFile id="fileToUpload" onchange="fileSelected();"/> <h:commandButton type="button" onclick="uploadFile()" value="Upload" /> </h:form> <hr/> <div id="uploadStatus"></div> <table> <tr> <td> <progress id="progressBar" style="visibility: hidden;" value="0" max="100"></progress> </td> <td> <div id="progressNumber" style="visibility: hidden;">0 %</div> </td> </tr> </table> <hr/> </h:body>

A possible output can be seen in the following screenshot:

The servlet behind this solution is UploadServlet that was presented earlier.

For multiple file uploads and progress bars, you can extend this example, or choose a built-in solution, such as PrimeFaces Upload, RichFaces Upload, or jQuery Upload Plugin.

Summary

In this article, we saw how to upload multiple files using JSF 2.2 and the concepts of indeterminate and determinate progress bars.

Resources for Article:


Further resources on this subject:


Books to Consider

comments powered by Disqus