Sabtu, 01 Februari 2014

FileServlet supporting resume and caching and GZIP






Introduction



In the almost 2 year old FileServlet and ImageServlet articles you can find basic examples of a download servlet and an image servlet. It does in fact nothing more than obtaining an InputStream of the desired resource/file and writing it to the OutputStream of the HTTP response along with a set of important response headers. It does not support resumes and effective caching of client side data.



If one downloaded a big file and got network problems on 99% of the file, one wouldn't be happy to discover the need to download the complete file again after getting network back. If a browser decides to check the cached images for changes, it would send a HEAD request to determine under each the unique file identifier and its timestamp or it would send a conditional GET request to determine the response status. If the image isn't changed according to the server response, the client won't re-request the image again to save the network bandwidth and other efforts.



You could leverage the task to a default servlet of the webcontainer/appserver you're using, but most of them doesn't implement all of the performance enhancements, so does for example Tomcat's DefaultServlet not support the Expires header.





Back to top


Resume downloads



To enable download resumes, the server have to send at least the Accept-Ranges, ETag and Last-Modified response headers to the client along with the file.



The Accept-Ranges response header with the value "bytes" informs the client that the server supports byte-range requests. With this the client could request for a specific byte range using the Range request header.



The ETag response header should contain a value which represents an unique identifier of the file in question so that both the server and the client can identify the file. You can use a combination of the file name, file size and file modification timestap for this. Some servers hauls this combination through a MD5 function to get an unique 32 character hexadecimal string. But this is not necessarily unique because two different strings could generate the same MD5 hash, so we won't use it here. The client could resend the obtained ETag back to the server for validation using the If-Match or If-Range request headers.



The Last-Modified response header should contain a date which represents the last modification timestamp of the file as it is at the server side. The client could resend the obtained timestamp back to the server for validation using the If-Unmodified-Since or If-Range request headers. Important note: keep in mind that the timestamp accuracy in server side Java is in milliseconds while the accurancy of the Last-Modified header is in seconds. In Java code you should add 1 second (1000ms) to the value of the If-* request headers to bridge this difference before validation.



Whenever the client sends a partial GET request with a Range request header to the server, then server should intercept on the conditional GET request headers (all headers starting with If) and handle accordingly. Whenever the If-Match or If-Unmodified-Since conditions are negative, the server should send a 412 "Precondition Failed" response back without any content. Whenever the If-Range condition is negative, then the server should ignore the Range header and send the full file back. Whenever the Range header is in invalid format, then the server should send a 416 "Requested Range Not Satisfiable" response back without any content.



If a partial GET request with a valid Range header is sent by the client, then the server should send the specific byte range(s) back as a 206 "Partial Content" response.



Back to top


Client side caching



The principle is the same as with resume downloads, with the only difference that no Range request header is been sent to the server. The server only have to check and validate any conditional GET request headers and respond accordingly. Usually those are the If-None-Match or If-Modified-Since request headers. The client could also send a HEAD request (for which the server should respond exactly like a GET, but completely without content) and determine the obtained ETag and Last-Modified response headers itself.



Whenever the If-None-Match or If-Modified-Since conditions are positive, the server should send a 304 "Not Modified" response back without any content. If this happens, then the client is allowed to use the content which is already available in the client side cache.



Further on you can use the Expires response header to inform the client how long to keep the content in the client side cache without firing any request about that, even no HEAD requests.



Back to top


GZIP compression



To save more network bandwitch, we could compress text files (text/javascript, text/css, text/xml, text/csv, etcetera) with GZIP. Generally you can save up to 70% of network bandwidth by compressing text files with GZIP. We only need to check if the client accepts GZIP encoding by checking if the Accept-Encoding header contains "gzip". If this is true, and the client is requesting the full file, then the full text file will be compressed. Statistics learn that about 90% of the browsers supports GZIP.



This may also be possible for all files other than text, but as it usually concerns images and another kinds of (large) binary files, it may unnecessarily generate too much overhead to (de)compress them.



Back to top


The Code



OK, enough boring technical background blah, now on to the code!


This fileservlet does everything what it should do based on the request headers as described above. It also supports multipart byte requests (the client could send multiple ranges commaseparated along with the Range header). The whole stuff is targeted on at least Java EE 5 and developed and tested in Eclipse 3.4 with Tomcat 6. It is tested with different webbrowsers (FireFox2/3, IE6/7/8, Opera8/9, Safari2/3 and Chrome) and also with a plain vanilla Java Application using URLConnection.


You can use it for any file types: binary files, text files, images, etcetera. When the requested file is a text file or an image or when its content type is covered by the Accept request header of the client, then it will be displayed inline, otherwise it will be sent as an attachment which will pop up a 'save as' dialogue.



It's almost 485 lines of code of which the nearly half are less or more rudimentary due to comments (read them all though), long-code line breaks and blank lines. You can just copy'n'paste and run it. You're free to make changes whenever needed as long as it's not for commercial use.



/*
* net/balusc/webapp/FileServlet.java
*
* Copyright (C) 2009 BalusC
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with this library.
* If not, see .
*/


package net.balusc.webapp;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* A file servlet supporting resume of downloads and client-side caching and GZIP of text content.
* This servlet can also be used for images, client-side caching would become more efficient.
* This servlet can also be used for text files, GZIP would decrease network bandwidth.
*
* @author BalusC
* @link http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html
*/

public class FileServlet extends HttpServlet {

// Constants ----------------------------------------------------------------------------------

private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.
private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

// Properties ---------------------------------------------------------------------------------

private String basePath;

// Actions ------------------------------------------------------------------------------------

/**
* Initialize the servlet.
* @see HttpServlet#init().
*/

public void init() throws ServletException {

// Get base path (path to get all resources from) as init parameter.
this.basePath = getInitParameter("basePath");

// Validate base path.
if (this.basePath == null) {
throw new ServletException("FileServlet init param 'basePath' is required.");
} else {
File path = new File(this.basePath);
if (!path.exists()) {
throw new ServletException("FileServlet init param 'basePath' value '"
+ this.basePath + "' does actually not exist in file system.");
} else if (!path.isDirectory()) {
throw new ServletException("FileServlet init param 'basePath' value '"
+ this.basePath + "' is actually not a directory in file system.");
} else if (!path.canRead()) {
throw new ServletException("FileServlet init param 'basePath' value '"
+ this.basePath + "' is actually not readable in file system.");
}
}
}

/**
* Process HEAD request. This returns the same headers as GET request, but without content.
* @see HttpServlet#doHead(HttpServletRequest, HttpServletResponse).
*/

protected void doHead(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
// Process request without content.
processRequest(request, response, false);
}

/**
* Process GET request.
* @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse).
*/

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
// Process request with content.
processRequest(request, response, true);
}

/**
* Process the actual request.
* @param request The request to be processed.
* @param response The response to be created.
* @param content Whether the request body should be written (GET) or not (HEAD).
* @throws IOException If something fails at I/O level.
*/

private void processRequest
(HttpServletRequest request, HttpServletResponse response, boolean content)
throws IOException
{
// Validate the requested file ------------------------------------------------------------

// Get requested file by path info.
String requestedFile = request.getPathInfo();

// Check if file is actually supplied to the request URL.
if (requestedFile == null) {
// Do your thing if the file is not supplied to the request URL.
// Throw an exception, or send 404, or show default/warning page, or just ignore it.
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

// URL-decode the file name (might contain spaces and on) and prepare file object.
File file = new File(basePath, URLDecoder.decode(requestedFile, "UTF-8"));

// Check if file actually exists in filesystem.
if (!file.exists()) {
// Do your thing if the file appears to be non-existing.
// Throw an exception, or send 404, or show default/warning page, or just ignore it.
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}

// Prepare some variables. The ETag is an unique identifier of the file.
String fileName = file.getName();
long length = file.length();
long lastModified = file.lastModified();
String eTag = fileName + "_" + length + "_" + lastModified;
long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;


// Validate request headers for caching ---------------------------------------------------

// If-None-Match header should contain "*" or ETag. If so, then return 304.
String ifNoneMatch = request.getHeader("If-None-Match");
if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", eTag); // Required in 304.
response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
return;
}

// If-Modified-Since header should be greater than LastModified. If so, then return 304.
// This header is ignored if any If-None-Match header is specified.
long ifModifiedSince = request.getDateHeader("If-Modified-Since");
if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setHeader("ETag", eTag); // Required in 304.
response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
return;
}


// Validate request headers for resume ----------------------------------------------------

// If-Match header should contain "*" or ETag. If not, then return 412.
String ifMatch = request.getHeader("If-Match");
if (ifMatch != null && !matches(ifMatch, eTag)) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}

// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return;
}


// Validate and process range -------------------------------------------------------------

// Prepare some variables. The full Range represents the complete file.
Range full = new Range(0, length - 1, length);
List<Range> ranges = new ArrayList<Range>();

// Validate and process Range and If-Range headers.
String range = request.getHeader("Range");
if (range != null) {

// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}

// If-Range header should either match ETag or be greater then LastModified. If not,
// then return full file.
String ifRange = request.getHeader("If-Range");
if (ifRange != null && !ifRange.equals(eTag)) {
try {
long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
ranges.add(full);
}
} catch (IllegalArgumentException ignore) {
ranges.add(full);
}
}

// If any valid If-Range header, then process each part of byte range.
if (ranges.isEmpty()) {
for (String part : range.substring(6).split(",")) {
// Assuming a file with length of 100, the following examples returns bytes at:
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
long start = sublong(part, 0, part.indexOf("-"));
long end = sublong(part, part.indexOf("-") + 1, part.length());

if (start == -1) {
start = length - end;
end = length - 1;
} else if (end == -1 || end > length - 1) {
end = length - 1;
}

// Check if Range is syntactically valid. If not, then return 416.
if (start > end) {
response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}

// Add range.
ranges.add(new Range(start, end, length));
}
}
}


// Prepare and initialize response --------------------------------------------------------

// Get content type by file name and set default GZIP support and content disposition.
String contentType = getServletContext().getMimeType(fileName);
boolean acceptsGzip = false;
String disposition = "inline";

// If content type is unknown, then set the default value.
// For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
// To add new content types, add new mime-mapping entry in web.xml.
if (contentType == null) {
contentType = "application/octet-stream";
}

// If content type is text, then determine whether GZIP content encoding is supported by
// the browser and expand content type with the one and right character encoding.
if (contentType.startsWith("text")) {
String acceptEncoding = request.getHeader("Accept-Encoding");
acceptsGzip = acceptEncoding != null && accepts(acceptEncoding, "gzip");
contentType += ";charset=UTF-8";
}

// Else, expect for images, determine content disposition. If content type is supported by
// the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
else if (!contentType.startsWith("image")) {
String accept = request.getHeader("Accept");
disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
}

// Initialize response.
response.reset();
response.setBufferSize(DEFAULT_BUFFER_SIZE);
response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("ETag", eTag);
response.setDateHeader("Last-Modified", lastModified);
response.setDateHeader("Expires", expires);


// Send requested file (part(s)) to client ------------------------------------------------

// Prepare streams.
RandomAccessFile input = null;
OutputStream output = null;

try {
// Open streams.
input = new RandomAccessFile(file, "r");
output = response.getOutputStream();

if (ranges.isEmpty() || ranges.get(0) == full) {

// Return full file.
Range r = full;
response.setContentType(contentType);
response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);

if (content) {
if (acceptsGzip) {
// The browser accepts GZIP, so GZIP the content.
response.setHeader("Content-Encoding", "gzip");
output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);
} else {
// Content length is not directly predictable in case of GZIP.
// So only add it if there is no means of GZIP, else browser will hang.
response.setHeader("Content-Length", String.valueOf(r.length));
}

// Copy full range.
copy(input, output, r.start, r.length);
}

} else if (ranges.size() == 1) {

// Return single part of file.
Range r = ranges.get(0);
response.setContentType(contentType);
response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
response.setHeader("Content-Length", String.valueOf(r.length));
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

if (content) {
// Copy single part range.
copy(input, output, r.start, r.length);
}

} else {

// Return multiple parts of file.
response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

if (content) {
// Cast back to ServletOutputStream to get the easy println methods.
ServletOutputStream sos = (ServletOutputStream) output;

// Copy multi part range.
for (Range r : ranges) {
// Add multipart boundary and header fields for every range.
sos.println();
sos.println("--" + MULTIPART_BOUNDARY);
sos.println("Content-Type: " + contentType);
sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

// Copy single part range of multi part range.
copy(input, output, r.start, r.length);
}

// End with multipart boundary.
sos.println();
sos.println("--" + MULTIPART_BOUNDARY + "--");
}
}
} finally {
// Gently close streams.
close(output);
close(input);
}
}

// Helpers (can be refactored to public utility class) ----------------------------------------

/**
* Returns true if the given accept header accepts the given value.
* @param acceptHeader The accept header.
* @param toAccept The value to be accepted.
* @return True if the given accept header accepts the given value.
*/

private static boolean accepts(String acceptHeader, String toAccept) {
String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
Arrays.sort(acceptValues);
return Arrays.binarySearch(acceptValues, toAccept) > -1
|| Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
|| Arrays.binarySearch(acceptValues, "*/*") > -1;
}

/**
* Returns true if the given match header matches the given value.
* @param matchHeader The match header.
* @param toMatch The value to be matched.
* @return True if the given match header matches the given value.
*/

private static boolean matches(String matchHeader, String toMatch) {
String[] matchValues = matchHeader.split("\\s*,\\s*");
Arrays.sort(matchValues);
return Arrays.binarySearch(matchValues, toMatch) > -1
|| Arrays.binarySearch(matchValues, "*") > -1;
}

/**
* Returns a substring of the given string value from the given begin index to the given end
* index as a long. If the substring is empty, then -1 will be returned
* @param value The string value to return a substring as long for.
* @param beginIndex The begin index of the substring to be returned as long.
* @param endIndex The end index of the substring to be returned as long.
* @return A substring of the given string value as long or -1 if substring is empty.
*/

private static long sublong(String value, int beginIndex, int endIndex) {
String substring = value.substring(beginIndex, endIndex);
return (substring.length() > 0) ? Long.parseLong(substring) : -1;
}

/**
* Copy the given byte range of the given input to the given output.
* @param input The input to copy the given range to the given output for.
* @param output The output to copy the given range from the given input for.
* @param start Start of the byte range.
* @param length Length of the byte range.
* @throws IOException If something fails at I/O level.
*/

private static void copy(RandomAccessFile input, OutputStream output, long start, long length)
throws IOException
{
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int read;

if (input.length() == length) {
// Write full range.
while ((read = input.read(buffer)) > 0) {
output.write(buffer, 0, read);
}
} else {
// Write partial range.
input.seek(start);
long toRead = length;

while ((read = input.read(buffer)) > 0) {
if ((toRead -= read) > 0) {
output.write(buffer, 0, read);
} else {
output.write(buffer, 0, (int) toRead + read);
break;
}
}
}
}

/**
* Close the given resource.
* @param resource The resource to be closed.
*/

private static void close(Closeable resource) {
if (resource != null) {
try {
resource.close();
} catch (IOException ignore) {
// Ignore IOException. If you want to handle this anyway, it might be useful to know
// that this will generally only be thrown when the client aborted the request.
}
}
}

// Inner classes ------------------------------------------------------------------------------

/**
* This class represents a byte range.
*/

protected class Range {
long start;
long end;
long length;
long total;

/**
* Construct a byte range.
* @param start Start of the byte range.
* @param end End of the byte range.
* @param total Total length of the byte source.
*/

public Range(long start, long end, long total) {
this.start = start;
this.end = end;
this.length = end - start + 1;
this.total = total;
}

}

}


In order to get the FileServlet to work, add the following entries to the Web Deployment Descriptor web.xml:




fileServlet
net.balusc.webapp.FileServlet

basePath
/path/to/files




fileServlet
/files/*


The basePath value must represent the absolute path to a folder containing all those files. You can of course change the value of the basePath parameter and the url-pattern of the servlet-mapping to your taste.



Here are some basic use examples:




href="files/foo.exe">download foo.exe
href="files/bar.zip">download bar.zip

src="files/pic.jpg" />
src="files/logo.gif" />


value="files/foo.exe">download foo.exe
value="files/bar.zip">download bar.zip
value="files/#{myBean.fileName}">
value="download #{myBean.fileName}" />


value="files/pic.jpg" />
value="files/logo.gif" />
value="files/#{myBean.imageFileName}" />


Important note: this servlet example does not take the requested file as request parameter, but just as part of the absolute URL, because a certain widely used browser developed by a team in Redmond would take the last part of the servlet URL path as filename during the 'Save As' dialogue instead of the in the headers supplied filename. Using the filename as part of the absolute URL (and thus not as request parameter) will fix this utterly stupid behaviour. As a bonus, the URL's look much nicer without query parameters.



Back to top


Copyright - GNU Lesser General Public License


(C) February 2009, BalusC




Source:http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html

Jumat, 31 Januari 2014

Effective datatable paging and sorting






Effective datatable paging and sorting at DAO level


In the 2 year old article Using datatables I wrote how to sort and page a JSF h:dataTable. Those are useful and nice if the dataset is small. But it is in fact less efficient as it first loads the whole data list from the database and uses Java to do the sorting and paging. It is much more efficient if you let the database do all the task. A self-respected database can sort the results much faster than Java can do. Querying a sublist from the database consumes much less memory in Java than when you query the complete list from the database. This all will make it much faster when you have a database with at least hundreds of rows.




Sorting using SQL can easily be done with the standardized ORDER BY clause. The way to obtain a subset of results differs per database. This article is targeted on MySQL. In MySQL you can obtain a subset of results with the LIMIT x, y clause. PostgreSQL uses LIMIT x OFFSET y. In Oracle you need to execute a ORDER BY subquery first and then use the ROWNUM clause on its results (SELECT * FROM (SELECT * FROM table ORDER BY column) WHERE ROWNUM BETWEEN x AND y). For MSSQL and DB2 you'll need to write a (w)hacky SQL query or to create a stored procedure. Consult Google or database specific documentation for details.


Back to top


Preparations


Next to a standard JSF implementation, we need the Tomahawk component library as it offers us the t:dataList and t:saveState components. The t:dataList is needed to display a collection of links with page numbers. It is preferred above JSTL's c:forEach, because it does its work more efficient. The t:saveState is needed to cache the displayed list and some important paging and sorting variables for the next request. It is preferred above h:inputHidden, because it does its work more efficient and it doesn't require a converter for non-standard object types. You can even cache a complete bean for the subsequent request, with which you can simulate a "conversation scope".


Integrating Tomahawk isn't that hard, you can even do that on a Sun Mojarra environment. You just need to add at least the following JAR's to the classpath, e.g. /WEB-INF/lib. The version numbers doesn't matter that much, as long as you get the newest.



The Tomahawk JAR is the Tomahawk component library itself which under each contains the t:dataList and t:saveState components. The commons JAR's are required by other components and/or the core of the Tomahawk component library.


Back to top


Backing Bean


Here is how the basic backing bean code look like. It is request scoped. If you're interested, an example of the DAOFactory can be found here: DAO tutorial - the data layer.



package mypackage;

import java.io.Serializable;
import java.util.List;

import javax.faces.component.UICommand;
import javax.faces.event.ActionEvent;

import mydao.DAOException;
import mydao.DAOFactory;
import mydao.MyDataDAO;
import mymodel.MyData;

/**
* The example backing bean for effective datatable paging and sorting.
*
* @author BalusC
* @link http://balusc.blogspot.com/2008/10/effective-datatable-paging-and-sorting.html
*/

public class MyBean implements Serializable {

// Properties ---------------------------------------------------------------------------------

// DAO.
private static MyDataDAO dao = DAOFactory.getInstance("javabase").getMyDataDAO();

// Data.
private List dataList;
private int totalRows;

// Paging.
private int firstRow;
private int rowsPerPage;
private int totalPages;
private int pageRange;
private Integer[] pages;
private int currentPage;

// Sorting.
private String sortField;
private boolean sortAscending;

// Constructors -------------------------------------------------------------------------------

public MyBean() {
// Set default values somehow (properties files?).
rowsPerPage = 10; // Default rows per page (max amount of rows to be displayed at once).
pageRange = 10; // Default page range (max amount of page links to be displayed at once).
sortField = "id"; // Default sort field.
sortAscending = true; // Default sort direction.
}

// Paging actions -----------------------------------------------------------------------------

public void pageFirst() {
page(0);
}

public void pageNext() {
page(firstRow + rowsPerPage);
}

public void pagePrevious() {
page(firstRow - rowsPerPage);
}

public void pageLast() {
page(totalRows - ((totalRows % rowsPerPage != 0) ? totalRows % rowsPerPage : rowsPerPage));
}

public void page(ActionEvent event) {
page(((Integer) ((UICommand) event.getComponent()).getValue() - 1) * rowsPerPage);
}

private void page(int firstRow) {
this.firstRow = firstRow;
loadDataList(); // Load requested page.
}

// Sorting actions ----------------------------------------------------------------------------

public void sort(ActionEvent event) {
String sortFieldAttribute = (String) event.getComponent().getAttributes().get("sortField");

// If the same field is sorted, then reverse order, else sort the new field ascending.
if (sortField.equals(sortFieldAttribute)) {
sortAscending = !sortAscending;
} else {
sortField = sortFieldAttribute;
sortAscending = true;
}

pageFirst(); // Go to first page and load requested page.
}

// Loaders ------------------------------------------------------------------------------------

private void loadDataList() {

// Load list and totalCount.
try {
dataList = dao.list(firstRow, rowsPerPage, sortField, sortAscending);
totalRows = dao.count();
} catch (DAOException e) {
throw new RuntimeException(e); // Handle it yourself.
}

// Set currentPage, totalPages and pages.
currentPage = (totalRows / rowsPerPage) - ((totalRows - firstRow) / rowsPerPage) + 1;
totalPages = (totalRows / rowsPerPage) + ((totalRows % rowsPerPage != 0) ? 1 : 0);
int pagesLength = Math.min(pageRange, totalPages);
pages = new Integer[pagesLength];

// firstPage must be greater than 0 and lesser than totalPages-pageLength.
int firstPage = Math.min(Math.max(0, currentPage - (pageRange / 2)), totalPages - pagesLength);

// Create pages (page numbers for page links).
for (int i = 0; i < pagesLength; i++) {
pages[i] = ++firstPage;
}
}

// Getters ------------------------------------------------------------------------------------

public List getDataList() {
if (dataList == null) {
loadDataList(); // Preload page for the 1st view.
}
return dataList;
}

public int getTotalRows() {
return totalRows;
}

public int getFirstRow() {
return firstRow;
}

public int getRowsPerPage() {
return rowsPerPage;
}

public Integer[] getPages() {
return pages;
}

public int getCurrentPage() {
return currentPage;
}

public int getTotalPages() {
return totalPages;
}

// Setters ------------------------------------------------------------------------------------

public void setRowsPerPage(int rowsPerPage) {
this.rowsPerPage = rowsPerPage;
}

}


Define it as usual in the faces-config.xml:






myBean
mypackage.MyBean
request




Back to top


Example DTO


Here is the basic DTO example. It's nothing special. It's just a dummy DTO with three fields: ID, Name and Value.



package mymodel;

import java.io.Serializable;

/**
* MyData. The example DTO (Data Transfer Object).
*
* @author BalusC
* @link http://balusc.blogspot.com/2008/10/effective-datatable-paging-and-sorting.html
*/

public class MyData implements Serializable {

// Properties ---------------------------------------------------------------------------------

private Long id;
private String name;
private Integer value;

// Constructors -------------------------------------------------------------------------------

public MyData() {
// Keep default constructor alive.
}

public MyData(Long id, String name, Integer value) {
this.id = id;
this.name = name;
this.value = value;
}

// Getters ------------------------------------------------------------------------------------

public Long getId() {
return id;
}

public String getName() {
return name;
}

public Integer getValue() {
return value;
}

// Setters ------------------------------------------------------------------------------------

public void setId(Long id) {
this.id = id;
}

public void setName(String name) {
this.name = name;
}

public void setValue(Integer value) {
this.value = value;
}

}


Back to top


Example DAO


The basic DAO example. Note that you cannot set the ORDER BY field and direction as PreparedStatement value. That's why it uses the String#format() for it. Keep SQL injection risks in mind. As long as the client can't control the values, you don't need to be afraid.


For more information and examples of the DAO layer and the DAOUtil class, you may find this article useful: DAO tutorial - the data layer.



package mydao;

import static mydao.DAOUtil.*;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import mymodel.MyData;

/**
* This class represents a SQL Database Access Object for the {@link MyData} DTO.
*
* @author BalusC
* @link http://balusc.blogspot.com/2008/10/effective-datatable-paging-and-sorting.html
*/

public final class MyDataDAO {

// Constants ----------------------------------------------------------------------------------

private static final String SQL_LIST_BY_ORDER_AND_LIMIT =
"SELECT id, name, value FROM mydata ORDER BY %s %s LIMIT ?, ?";
private static final String SQL_COUNT =
"SELECT count(*) FROM mydata";

// Properties ---------------------------------------------------------------------------------

private DAOFactory daoFactory;

// Constructors -------------------------------------------------------------------------------

/**
* Construct MyData DAO for the given DAOFactory. Package private so that it can be constructed
* inside the DAO package only.
* @param daoFactory The DAOFactory to construct this MyData DAO for.
*/

MyDataDAO(DAOFactory daoFactory) {
this.daoFactory = daoFactory;
}

// Actions ------------------------------------------------------------------------------------

/**
* Returns list of MyData items starting at the given first index with the given row count,
* sorted by the given sort field and sort order.
* @param firstRow First index of rows to be returned.
* @param rowCount Amount of rows to be returned.
* @param sortField Field to sort the data on.
* @param sortAscending Whether to sort data ascending or not.
* @return list of MyData items starting at the given first index with the given row count,
* sorted by the given sort field and sort order.
* @throws DAOException If something fails at DAO level.
*/

public List list(int firstRow, int rowCount, String sortField, boolean sortAscending)
throws DAOException
{
Object[] values = { firstRow, rowCount };

String sortDirection = sortAscending ? "ASC" : "DESC";
String sql = String.format(SQL_LIST_BY_ORDER_AND_LIMIT, sortField, sortDirection);
List dataList = new ArrayList<>();

try (
Connection connection = daoFactory.getConnection();
PreparedStatement statement = prepareStatement(connection, sql, false, values);
ResultSet resultSet = statement.executeQuery();
) {
while (resultSet.next()) {
dataList.add(mapMyData(resultSet));
}
} catch (SQLException e) {
throw new DAOException(e);
}

return dataList;
}

/**
* Returns total amount of rows in table.
* @return Total amount of rows in table.
* @throws DAOException If something fails at DAO level.
*/

public int count() throws DAOException {
int count = 0;

try (
Connection connection = daoFactory.getConnection();
PreparedStatement statement = connection.prepareStatement(SQL_COUNT);
ResultSet resultSet = statement.executeQuery();
) {
if (resultSet.next()) {
count = resultSet.getInt(1);
}
} catch (SQLException e) {
throw new DAOException(e);
}

return count;
}

/**
* Map the current row of the given ResultSet to MyData.
* @param resultSet The ResultSet of which the current row is to be mapped to MyData.
* @return The mapped MyData from the current row of the given ResultSet.
* @throws SQLException If something fails at database level.
*/

private static MyData mapMyData(ResultSet resultSet) throws SQLException {
return new MyData(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getObject("value") != null ? resultSet.getInt("value") : null
);
}

}


Back to top


JSF file


And now the JSF file, it has a sortable datatable, a bunch of paging buttons (first, previous, next and last), the status of current page and total pages, a bunch of links pointing to a specific page and finally a input field where you can specify the amount of rows to be displayed at once.



<%@taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<%@taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@taglib uri="http://myfaces.apache.org/tomahawk" prefix="t"%>

html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">


xmlns="http://www.w3.org/1999/xhtml">

</span>Effective datatable paging and sorting at DAO level<span class="codetag">


id="form">

<%-- The sortable datatable --%>
value="#{myBean.dataList}" var="item">

name="header">
value="ID" actionListener="#{myBean.sort}">
name="sortField" value="id" />


value="#{item.id}" />


name="header">
value="Name" actionListener="#{myBean.sort}">
name="sortField" value="name" />


value="#{item.name}" />


name="header">
value="Value" actionListener="#{myBean.sort}">
name="sortField" value="value" />


value="#{item.value}" />



<%-- The paging buttons --%>
value="first" action="#{myBean.pageFirst}"
disabled="#{myBean.firstRow == 0}" />
value="prev" action="#{myBean.pagePrevious}"
disabled="#{myBean.firstRow == 0}" />
value="next" action="#{myBean.pageNext}"
disabled="#{myBean.firstRow + myBean.rowsPerPage >= myBean.totalRows}" />
value="last" action="#{myBean.pageLast}"
disabled="#{myBean.firstRow + myBean.rowsPerPage >= myBean.totalRows}" />
value="Page #{myBean.currentPage} / #{myBean.totalPages}" />



<%-- The paging links --%>
value="#{myBean.pages}" var="page">
value="#{page}" actionListener="#{myBean.page}"
rendered="#{page != myBean.currentPage}" />
value="#{page}"
escape="false"
rendered="#{page == myBean.currentPage}" />




<%-- Set rows per page --%>
for="rowsPerPage" value="Rows per page" />
id="rowsPerPage" value="#{myBean.rowsPerPage}" size="3" maxlength="3" />
value="Set" action="#{myBean.pageFirst}" />
for="rowsPerPage" errorStyle="color: red;" />

<%-- Cache bean with data list, paging and sorting variables for next request --%>
value="#{myBean}" />





Save it as paging.jsp or so and invoke it by http://localhost:8080/playground/paging.jsf, assuming that your development server runs at port 8080 and the playground environment's context root is called 'playground'.



That's all!


Back to top


Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.


(C) October 2008, BalusC



Source:http://balusc.blogspot.com/2008/10/effective-datatable-paging-and-sorting.html

Validate required checkbox






Introduction


The required attribute of a h:selectBooleanCheckbox is a bit non-intuitive. If you want to require the user to check the desired checkbox, you would assume that setting the required attribute to true ought to be sufficient.




But it is not. As for every other UIInput component the default required="true" validator would only check if the value is actually filled and been sent to the server side, i.e. the value is not null nor empty. In case of a h:selectBooleanCheckbox, which accepts Boolean or boolean properties only, JSF EL will coerce the unchecked value to Boolean.FALSE during apply request values phase, right before validations phase. This value is not null nor empty! Thus, the required attribute of the h:selectBooleanCheckbox is fairly pointless. It would always pass the validation and thus never display the desired required message in case of an unchecked checkbox.


Back to top


RequiredCheckboxValidator


To solve this non-intuitive behaviour (I am still not sure if this is a bug or a feature; the coercion of a null or empty Boolean property to Boolean.FALSE instead of null might be a bug, but this is not the case when you used boolean; after all I would consider it as an unwanted feature which should be better documented), best what you can do is to create your own javax.faces.validator.Validator implementation specific for a h:selectBooleanCheckbox of which a checked value is required. It is relatively simple, just check if the provided value parameter equals Boolean.FALSE and handle accordingly.



package mypackage;

import java.text.MessageFormat;

import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;

public class RequiredCheckboxValidator implements Validator {

public void validate(FacesContext context, UIComponent component, Object value)
throws ValidatorException
{
if (value.equals(Boolean.FALSE)) {
String requiredMessage = ((UIInput) component).getRequiredMessage();

if (requiredMessage == null) {
Object label = component.getAttributes().get("label");
if (label == null || (label instanceof String && ((String) label).length() == 0)) {
label = component.getValueExpression("label");
}
if (label == null) {
label = component.getClientId(context);
}
requiredMessage = MessageFormat.format(UIInput.REQUIRED_MESSAGE_ID, label);
}

throw new ValidatorException(
new FacesMessage(FacesMessage.SEVERITY_ERROR, requiredMessage, requiredMessage));
}
}

}

Note that this validator checks if the developer has set any requiredMessage attribute so that it uses its value instead of the JSF default required message.



Here is how you should define it in the faces-config.xml:




requiredCheckboxValidator
mypackage.RequiredCheckboxValidator


That's it! Attach this validator to the h:selectBooleanCheckbox using f:validator.


Back to top


Basic demonstration example



Here is a sample form. It represents a kind of an agreement form which requires the checkbox being checked before submit. This is not an uncommon functional requirement.


The stuff is developed and tested in a Java EE 5.0 environment with Tomcat 6.0.18 with Servlet 2.5, JSP 2.1 and JSF 1.2_09.




for="agree" value="Do you agree with me?" />
id="agree" value="#{myBean.agree}" requiredMessage="You must agree!">
validatorId="requiredCheckboxValidator" />

value="Submit" action="#{myBean.submit}" />
infoStyle="color: green;" errorStyle="color: red;" />


The appropriate test backing bean (request scoped) look like:



package mypackage;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;

public class MyBean {

// Properties ---------------------------------------------------------------------------------

private Boolean agree;

// Actions ------------------------------------------------------------------------------------

public void submit() {
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage("You agreed with me!"));
}

// Getters ------------------------------------------------------------------------------------

public Boolean getAgree() {
return agree;
}

// Setters ------------------------------------------------------------------------------------

public void setAgree(Boolean agree) {
this.agree = agree;
}

}


Now, when you submit the form with an checked checkbox, JSF will just proceed with form processing, otherwise it will display the desired required message!



Back to top



Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.


(C) September 2008, BalusC




Source:http://balusc.blogspot.com/2008/09/validate-required-checkbox.html

Styling options in h:selectOneMenu






Introduction


Whenever you want to style a HTML element using CSS, you could just use its style or, preferably, class attribute. But in the default Sun JSF Mojarra implementation there is no comparable attribute available for that. The h:selectOneMenu, h:selectManyMenu and f:selectItem tags simply doesn't support it.



When looking at comparable attributes in other elements, you'll notice that h:dataTable has an elegant approach in form of the rowClasses attribute which accepts a commaseparated string of CSS class names which are to be applied on the elements repeatedly. Now, it would be nice to let among others the h:selectOneMenu support a similar optionClasses attribute.


This can be achieved at two ways: overriding the default renderer class and using the f:attribute to add it as an external component attribute, or overriding the default renderer class, the component class and the tag class to let it support the optionClasses attribute. It might be obvious that the first way is a bit hacky, but it costs much less effort. The second way is more elegant, but it require more code and a custom tld file which should copy all existing component attributes over (tld files unfortunately doesn't know anything about inheritance). BalusC did it and the tld file was almost 500 lines long for only the selectOneMenu and selectManyMenu. Ouch.


This article will handle only the first approach in detail.


Back to top


ExtendedMenuRenderer


Here is how the extended MenuRenderer look like:



package net.balusc.jsf.renderer.html;

import java.io.IOException;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.model.SelectItem;

import com.sun.faces.renderkit.html_basic.MenuRenderer;

/**
* Extended menu renderer which renders the 'optionClasses' attribute above the standard menu
* renderer. To use it, define it as follows in the render-kit tag of faces-config.xml.
*
*

* <renderer>
* <component-family>javax.faces.SelectOne</component-family>
* <renderer-type>javax.faces.Menu</renderer-type>
* <renderer-class>net.balusc.jsf.renderer.html.ExtendedMenuRenderer</renderer-class>
* </renderer>
* <renderer>
* <component-family>javax.faces.SelectMany</component-family>
* <renderer-type>javax.faces.Menu</renderer-type>
* <renderer-class>net.balusc.jsf.renderer.html.ExtendedMenuRenderer</renderer-class>
* </renderer>
*

*
* And define the 'optionClasses' attribute as a f:attribute of the h:selectOneMenu or
* h:selectManyMenu as follows:
*
*

* <f:attribute name="optionClasses" value="option1,option2,option3" />
*

*
* It accepts a comma separated string of CSS class names which are to be applied on the options
* repeatedly (the same way as you use rowClasses in h:dataTable). The optionClasses will be
* rendered only if there is no 'disabledClass' or 'enabledClass' being set as an attribute.
*
* @author BalusC
* @link http://balusc.blogspot.com/styling-options-in-hselectonemenu.html
*/

public class ExtendedMenuRenderer extends MenuRenderer {

// Override -----------------------------------------------------------------------------------

/**
* @see com.sun.faces.renderkit.html_basic.MenuRenderer#renderOption(
* javax.faces.context.FacesContext, javax.faces.component.UIComponent,
* javax.faces.convert.Converter, javax.faces.model.SelectItem, java.lang.Object,
* java.lang.Object[])
*/

protected void renderOption(FacesContext context, UIComponent component, Converter converter,
SelectItem currentItem, Object currentSelections, Object[] submittedValues)
throws IOException
{
// Copied from MenuRenderer#renderOption() (and a bit rewritten, but that's just me) ------

// Get writer.
ResponseWriter writer = context.getResponseWriter();
assert (writer != null);

// Write 'option' tag.
writer.writeText("\t", component, null);
writer.startElement("option", component);

// Write 'value' attribute.
String valueString = getFormattedValue(context, component, currentItem.getValue(), converter);
writer.writeAttribute("value", valueString, "value");

// Write 'selected' attribute.
Object valuesArray;
Object itemValue;
if (containsaValue(submittedValues)) {
valuesArray = submittedValues;
itemValue = valueString;
} else {
valuesArray = currentSelections;
itemValue = currentItem.getValue();
}
if (isSelected(context, itemValue, valuesArray)) {
writer.writeAttribute("selected", true, "selected");
}

// Write 'disabled' attribute.
Boolean disabledAttr = (Boolean) component.getAttributes().get("disabled");
boolean componentDisabled = disabledAttr != null && disabledAttr.booleanValue();
if (!componentDisabled && currentItem.isDisabled()) {
writer.writeAttribute("disabled", true, "disabled");
}

// Write 'class' attribute.
String labelClass;
if (componentDisabled || currentItem.isDisabled()) {
labelClass = (String) component.getAttributes().get("disabledClass");
} else {
labelClass = (String) component.getAttributes().get("enabledClass");
}

// Inserted custom code which checks the optionClasses attribute --------------------------

if (labelClass == null) {
String optionClasses = (String) component.getAttributes().get("optionClasses");
if (optionClasses != null) {
String[] labelClasses = optionClasses.split("\\s*,\\s*");
String indexKey = component.getClientId(context) + "_currentOptionIndex";
Integer index = (Integer) component.getAttributes().get(indexKey);
if (index == null || index == labelClasses.length) {
index = 0;
}
labelClass = labelClasses[index];
component.getAttributes().put(indexKey, ++index);
}
}

// The remaining copy of MenuRenderer#renderOption() --------------------------------------

if (labelClass != null) {
writer.writeAttribute("class", labelClass, "labelClass");
}

// Write option body (the option label).
if (currentItem.isEscape()) {
String label = currentItem.getLabel();
if (label == null) {
label = valueString;
}
writer.writeText(label, component, "label");
} else {
writer.write(currentItem.getLabel());
}

// Write 'option' end tag.
writer.endElement("option");
writer.writeText("\n", component, null);
}

}


Configure it as follows in the faces-config.xml:





javax.faces.SelectOne
javax.faces.Menu
net.balusc.jsf.renderer.html.ExtendedMenuRenderer


javax.faces.SelectMany
javax.faces.Menu
net.balusc.jsf.renderer.html.ExtendedMenuRenderer



That's all!



Back to top


Basic demonstration example


And now a basic demonstration example how to use it.


The relevant part of the JSF file should look like:




value="#{myBean.selectedItem}">
name="optionClasses" value="option1, option2" />
value="#{myBean.selectItems}" />

value="submit" action="#{myBean.submit}" />


Note the f:attribute: this sets the optionClasses attribute value which is been picked up by the ExtendedMenuRenderer. It will apply the given CSS style classes repeatedly on the rendered option elements. You can even use EL in it so that a backing bean can generate the desired String of comma separated CSS style classes based on some conditions.



The CSS styles are definied as follows:



option.option1 {
background-color: #ccc;
}

option.option2 {
background-color: #fcc;
}


Note that some web browsers wouldn't apply this on the selected option in the h:selectOneMenu. If desired, you need to add a style class for the