This post was updated here:
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):
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())) {
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