even more powerful if we use this paging pattern with hibernate dynamic queries(H3).
DAO,
Code:
public Map findItemsByCriteriaWithTotal(ItemCommand itemCommand)
throws DataAccessException {
ArrayList items = (ArrayList) findItemsByCriteriaWithLimit(itemCommand);
Integer totalFound = findItemsTotalByCriteria(itemCommand);
Map model = new HashMap();
model.put("_totalElements", totalFound);
model.put("_resultset", items);
return model;
}
public Collection findItemsByCriteriaWithLimit(ItemCommand itemCommand)
throws DataAccessException {
final Item item = itemCommand.getItem();
final Book ibook = item.getBook();
final String sortColumn = itemCommand.getS_sortColumn();
final int page = itemCommand.getS_page();
final int pageSize = itemCommand.getS_pageSize();
final int ascending = itemCommand.getS_ascending();
return getHibernateTemplate().executeFind(new HibernateCallback() {
public Object doInHibernate(Session session)
throws HibernateException, SQLException {
Example exampleItem = Example.create(item).ignoreCase()
.enableLike(MatchMode.ANYWHERE);
Example exampleBook = Example.create(ibook).ignoreCase()
.enableLike(MatchMode.ANYWHERE);
Criteria criteria = session.createCriteria(Item.class)
.add(exampleItem);
if (ascending > 0) {
criteria.addOrder(Order.asc(sortColumn));
} else {
criteria.addOrder(Order.desc(sortColumn));
}
criteria.createCriteria("book")
.add(exampleBook)
.setFirstResult(page * pageSize)
.setMaxResults(pageSize);
return criteria.list();
}
});
}
public Integer findItemsTotalByCriteria(ItemCommand itemCommand)
throws DataAccessException {
final Item item = itemCommand.getItem();
final Book ibook = item.getBook();
return (Integer) getHibernateTemplate().execute(
new HibernateCallback() {
public Object doInHibernate(Session session)
throws HibernateException, SQLException {
Example exampleItem = Example.create(item).ignoreCase()
.enableLike(MatchMode.ANYWHERE);
Example exampleBook = Example.create(ibook)
.ignoreCase().enableLike(MatchMode.ANYWHERE);
Integer count = (Integer) session.createCriteria(Item.class)
.setProjection(Projections.rowCount())
.add(exampleItem)
.createCriteria("book")
.add(exampleBook)
.uniqueResult();
return count;
}
});
}
Domain 1 - Item.java
Code:
public class Item implements java.io.Serializable {
// Fields
private Long id;
private Long version;
private String barcode;
private String shelfMark;
private Date lastCheckin;
private Borrow borrow;
private Book book;
private ItemDuration itemDuration;
private ItemType itemType;
private Location location;
private ItemStatusType itemStatusType;
// get ,set
......
}
Domain 2 - Book.java
Code:
public class Book implements java.io.Serializable {
// Fields
private Long id;
private Long version;
private String title;
private Integer publishingYear;
private String isbn;
private String edition;
private String description;
private String publicationPlace;
private String classMark;
private String attachment;
private Set items;
private Set authors = new HashSet();
private Set subjects = new HashSet();
private Set seriez = new HashSet();
private Set otherTitles = new HashSet();
private Set reservations;
private Note note;
private Publisher publisher;
// get , set
......
}
Controller , (AbstractPagingFormController extends AbstractLibraryFormController , and AbstractLibraryFormController extends SimpleFormController)
Code:
public class ItemSearch extends AbstractPagingFormController {
private String itemColumnKey;
public ItemSearch() {
setCommandClass(ItemCommand.class);
setCommandName("itemCommand");
setSessionForm(true);
}
public void setItemColumnKey(String itemColumnKey) {
this.itemColumnKey = itemColumnKey;
}
public ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
ItemCommand itemCommand = (ItemCommand) command;
Map model = getLibrary().findItemsByCriteriaWithTotal(itemCommand);
ArrayList items = (ArrayList) model.get("_resultset");
if (items.size() < 1) {
// no item found
return new ModelAndView(getSuccessView(), "NoItemFound",
"notEmptyString");
}
// multiple items found
return new ModelAndView(getSuccessView(), "Items", model);
}
protected void onBindAndValidate(HttpServletRequest request,
Object command, BindException errors) throws Exception {
String sortColumn = ((ItemCommand) command).getS_sortColumn();
if (!PagingUtil.IsSortColumnMatch(sortColumn, itemColumnKey)) {
// Reconsider !!
errors.rejectValue("s_sortColumn", "SortColumnNotMatch",
"Action and SortColumn Not Match !");
}
}
AbstractPagingFormController.java ,
Code:
public abstract class AbstractPagingFormController extends AbstractLibraryFormController {
// Just for Paging
protected boolean isFormSubmission(HttpServletRequest request) {
if ("POST".equals(request.getMethod())) return true ;
Enumeration paramNames = request.getParameterNames();
while(paramNames.hasMoreElements()) {
String parm = (String)paramNames.nextElement();
if (parm.startsWith("s_")) return true;
}
return false;
}
}
itemColumnKey is use to block direct url writing in browser's address bar.
library-servlet.xml ,
Code:
<bean id="simplePagingPropertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>/WEB-INF/sortColumn.properties</value>
</list>
</property>
</bean>
.....
<bean id="itemSearchForm" class="org.yourschool.library.web.ItemSearch">
<property name="library"><ref bean="library"/></property>
<property name="validator"><ref bean="beanValidator"/></property>
<property name="formView"><value>itemSearchForm</value></property>
<property name="successView"><value>item.results</value></property>
<property name="itemColumnKey"><value>${itemSC}</value></property>
// *******************************************************
</bean>
PagingUtil.java ,
Code:
public class PagingUtil {
private static String delimiter = ",";
public void setDelimiter(String delimiter) {
PagingUtil.delimiter = delimiter;
}
public static boolean IsSortColumnMatch(String sortColumn,String key) {
boolean isSortColumnMatch = false;
StringTokenizer st = new StringTokenizer(key,delimiter);
while (st.hasMoreTokens()) {
if (st.nextToken().equals(sortColumn)) {
isSortColumnMatch = true;
break;
}
}
return isSortColumnMatch;
}
}
sortColumn.properties ,
Code:
#Created by JInto - www.guh-software.de
#Tue Aug 30 18:39:33 SGT 2005
authorSC=id,authorName
bookSC=id,title,publishingYear,isbn,edition,description,publicationPlace,classMark,attachment
itemSC=id,barcode,shelfMark,lastCheckin
otherTitleSC=id,otherTitleName
publisherSC=id,publisherName
seriesSC=id,seriesName
subjectSC=id,subjectName
commond validator constrains to parameter s_sortColumn , s_page , ...
Code:
<formset>
<form name="itemCommand">
<field property="s_sortColumn" depends="required">
<arg0 key="_sortColumn"/>
</field>
<field property="s_page" depends="required,integer,intRange">
<arg0 key="_page"/>
<arg1 name="intRange" key="${var:min}" resource="false"/>
<arg2 name="intRange" key="${var:max}" resource="false"/>
<var><var-name>min</var-name><var-value>0</var-value></var>
<var><var-name>max</var-name><var-value>1000000000</var-value></var>
</field>
<field property="s_pageSize" depends="required,integer,intRange">
<arg0 key="_pageSize"/>
<arg1 name="intRange" key="${var:min}" resource="false"/>
<arg2 name="intRange" key="${var:max}" resource="false"/>
<var><var-name>min</var-name><var-value>0</var-value></var>
<var><var-name>max</var-name><var-value>50</var-value></var>
</field>
<field property="s_ascending" depends="required,integer,intRange">
<arg0 key="_ascending"/>
<arg1 name="intRange" key="${var:min}" resource="false"/>
<arg2 name="intRange" key="${var:max}" resource="false"/>
<var><var-name>min</var-name><var-value>0</var-value></var>
<var><var-name>max</var-name><var-value>1</var-value></var>
</field>
</form>
</formset>
Command - ItemCommand.java ,
Code:
public class ItemCommand implements Serializable {
private int page;
private int pageSize;
private int ascending;
private String sortColumn;
private Item item;
public ItemCommand() {
this.item = new Item();
Date lastCheckin = null;
item.setLastCheckin(lastCheckin);
Book book = new Book();
item.setBook(book);
}
..... // get , set
}
GoogleTag.java (minor change),
Code:
public class GoogleTag extends BodyTagSupport {
private Map parameters;
private String modelName;
private int listSize;
// Some databases are not allow pageSize to be negative (Hibernate's MaxResults has to be >= 0)
private int pageSize;
private int page;
private int totalElements;
private int pagingWidth = 11;
//TODO pagingWidth cannot be negative !!
private String previousPageLink;
private String nextPageLink;
//private LinkEncoder UrlEncoder = new LinkEncoder();
public static final String PAGE = "s_page";
public static final String PAGE_SIZE = "s_pageSize";
public void setModelName(String modelName){
this.modelName = modelName;
}
public void setPagingWidth(int pagingWidth){
this.pagingWidth = pagingWidth;
}
public int doStartTag() throws JspException {
initProperties();
if(pageSize > 0){
try{
writePaging();
}catch(IOException ex) {
ex.getMessage();
}
}
return SKIP_BODY;
}
public int doEndTag() throws JspException {
return EVAL_PAGE;
}
public void initProperties(){
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
this.parameters = RequestUtil.getRequestParameterMap(request);
String Page = (String)parameters.get(PAGE);
// The reason for doing this is due to the page , pageSize , ascending are int and NOT Integer(Object) ,
// then "0" will be given by the Spring binder if clients are not supply the parameter name (not parameter value).
if (Page == null || Page.equals("")){
this.page = 0;
}else{
this.page = Integer.parseInt(Page);
}
String PageSize = (String)parameters.get(PAGE_SIZE);
if (PageSize == null || PageSize.equals("")){
this.pageSize = 0;
}else{
this.pageSize = Integer.parseInt(PageSize);
}
this.listSize = ((List)((Map)request.getAttribute(modelName)).get("_resultset")).size();
this.totalElements = ((Integer)((Map)request.getAttribute(modelName)).get("_totalElements")).intValue();
this.previousPageLink = LinkEncoder.encode(makePreviousPageParameters(parameters));
this.nextPageLink = LinkEncoder.encode(makeNextPageParameters(parameters));
}
public void writePaging() throws IOException {
StringBuffer sb = new StringBuffer();
// Declare Variable
int totalPages ;
if(totalElements%pageSize == 0){
totalPages = (totalElements/pageSize) ;
}else{
totalPages = (totalElements/pageSize) + 1 ;
}
int pageA = (pagingWidth/2) + 1 ;
int pageC = totalPages - pageA ;
int lastPage = totalPages - 1 ;
//Display total number
sb.append("|").append(totalElements).append("|.......");
//.append(page+1).append("/").append(totalPages).append(".......");
if (isPreviousPage()) {
sb.append("<a href=\"?").append(previousPageLink);
sb.append("\"><font color=\"blue\"><B><<<</B></font></a>\n");
//sb.append("\"><img alt=\"Next\" src=\"../img/btn_prev.gif\" border=\"0\"/></a>\n");
}
sb.append("");
//Reminder : First page in Hibernate ---> page = 0 , not page = 1 !!
/**
page : 0 , 1 , 2 , ..... , pageA , pageA + 1 , ..... , pageC -1 , pageC , ....., totalPages - 1
| | | |
firstPage | | lastPage
page = 0 | | page = totalPages - 1
| | | |
|-------- Region A -------|----------- Region B ---------|-------- Region C -------|
| 0 <= page <= pageA | pageA < page < pageC |pageC <= page <= lastPage|
**/
//Case 1 : if lastPage < pagingWidth + 2
if ( lastPage < pagingWidth + 2 ){
for( int i = 0 ; i < lastPage + 1 ; i++ ){
if( page == i ){
sb.append(page+1).append("");
}else{
sb = numberLinkMaker(sb , makePageLink(parameters,i) , i+1);
}
}
}else{
//Case 2 : if lastPage => pagingWidth + 2
//First Page
if( page == 0 ){
sb.append(page+1).append("");
}else{
sb = numberLinkMaker(sb , makePageLink(parameters,0) , 1);
}
//if page in Region A
if ( 0 <= page & page <= pageA ){
for( int i = 1 ; i <= pagingWidth ; i++ ){
if( page == i ){
sb.append(page+1).append("");
}else{
sb = numberLinkMaker(sb , makePageLink(parameters,i) , i+1);
}
}
sb.append("...");
//if page in Region B
}else if ( pageA < page & page < pageC){
sb.append("...");
for ( int i = page - pagingWidth/2 ; i <= page + pagingWidth/2 ; i++ ){
if( page == i ){
sb.append(page+1).append("");
}else{
sb = numberLinkMaker(sb , makePageLink(parameters,i) , i+1);
}
}
if(page != pageC - 1)sb.append("...");
//if page in Region C
}else if ( pageC <= page & page <= lastPage ){
sb.append("...");
for( int i = lastPage - pagingWidth ; i < lastPage ; i++ ){
if( page == i ){
sb.append(page+1).append("");
}else{
sb = numberLinkMaker(sb , makePageLink(parameters,i) , i+1);
}
}
}
//Last Page
if( page == lastPage ){
sb.append(page+1).append("");
}else{
sb = numberLinkMaker(sb , makePageLink(parameters,lastPage) , lastPage + 1 );
}
}
if(totalElements%pageSize == 0 & page == lastPage){
//Do Nothing
}else{
if (isNextPage()) {
sb.append("<a href=\"?").append(nextPageLink);
sb.append("\"><font color=\"blue\"><B>>>></B></font></a>\n");
//sb.append("\"><img alt=\"Next\" src=\"../img/btn_next.gif\" border=\"0\"/></a>\n");
}
}
//Print out to jsp
pageContext.getOut().print(sb.toString());
}
public String makePageLink(Map parameters, int pageNumber){
parameters.put(PAGE,""+pageNumber);
return LinkEncoder.encode(parameters);
}
public StringBuffer numberLinkMaker(StringBuffer stringBuffer , String pageLink , int pageNumber){
stringBuffer.append("<a href=\"?").append(pageLink);
stringBuffer.append("\"><font color=\"blue\"><B>").append(pageNumber).append("</B></font></a>\n");
stringBuffer.append("");
return stringBuffer;
}
public Map makePreviousPageParameters(Map parameters){
int previousPage;
previousPage = page-1;
//NumberFormatException
parameters.put(PAGE,""+previousPage);
return parameters;
}
public Map makeNextPageParameters(Map parameters){
int nextPage;
nextPage = page+1;
parameters.put(PAGE,""+nextPage);
return parameters;
}
public boolean isNextPage() {
return listSize > pageSize-1;
}
public boolean isPreviousPage() {
return page > 0;
}
}
SortingTag.java (minor change) ,
Code:
public class SortingTag extends MessageTag {
private Map parameters;
private String sortColumn;
private String sortColumnLink;
private String ascending;
public static final String PAGE = "s_page";
public static final String SORT_COLUMN = "s_sortColumn";
public static final String ASCENDING = "s_ascending";
//private LinkEncoder UrlEncoder = new LinkEncoder();
public void setSortColumn(String sortColumn){
this.sortColumn = sortColumn;
}
public void writeMessage(String msg) throws IOException {
initProperties();
try{
decorateMessage(msg);
}
catch(IOException ex) {
ex.getMessage();
}
}
public void initProperties(){
HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
this.parameters = RequestUtil.getRequestParameterMap(request);
this.ascending = (String)parameters.get(ASCENDING);
//this.sortColumnLink = UrlEncoder.encode(makeSortColumnParameters(parameters));
this.sortColumnLink = LinkEncoder.encode(makeSortColumnParameters(parameters));
}
public void decorateMessage(String msg) throws IOException {
StringBuffer sb = new StringBuffer();
sb.append("<a href=\"?").append(sortColumnLink);
sb.append("\"><font color=\"blue\"><B>").append(msg).append("</B></font></a>");
pageContext.getOut().print(sb.toString());
}
public Map makeSortColumnParameters(Map parameters){
int firstPage = 0;
// NumberFormatException
parameters.put(PAGE,""+firstPage);
// Cannot change order of the following two line of codes , Differents Behaviour !!
String sortColumnFromRequest = (String)parameters.get(SORT_COLUMN);
parameters.put(SORT_COLUMN,sortColumn);
if (sortColumn.equals(sortColumnFromRequest)) {
// Just to prevent NullPointerException (NPE)
if(ascending == null || ascending.equals("")) ascending = "0";
// 1 is true , 0 is false
if(ascending.equals("1")){
parameters.put(ASCENDING,"0");
}else {
parameters.put(ASCENDING,"1");
}
}else{
parameters.put(ASCENDING,"1");
}
return parameters;
}
}
ItemSearchForm.jsp (this search page is easy enough , but it is powerful to do any item related search , for example - search item by book's classMark)
Code:
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ include file="/WEB-INF/jsp/includeTop.jsp" %>
<html>
<head>
<title>Welcome to YourSchool's Library System</title>
</head>
<body>
<div align="center">
<form name="searchItemForm" method="post" action="<c:url value='/admin/itemSearchForm.htm'/>">
<spring:message code="search"/> :
<input type="hidden" name="s_page" value="0"/>
<input type="hidden" name="s_pageSize" value="15"/>
<input type="hidden" name="s_sortColumn" value="barcode"/>
<input type="hidden" name="s_ascending" value="1"/>
<table width="70%" border="0">
<tr>
<td>barcode:</td>
<td><input type="text" size="40" name="item.barcode"/></td>
</tr>
<tr>
<td>shelfmark:</td>
<td><input type="text" size="40" name="item.shelfMark"/></td>
</tr>
<tr>
<td>book title:</td>
<td><input type="text" size="40" name="item.book.title"/></td>
</tr>
<tr>
<td>classMark:</td>
<td><input type="text" size="40" name="item.book.classMark"/></td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>
<input type="button" onclick="javascript:document.searchItemForm.submit()" value='<spring:message code="search"/>'/>
<c:if test="${!empty itemCommand}" >
<spring:bind path="itemCommand.*">
<c:forEach var="error" items="${status.errorMessages}">
<b><font color=red>
<BR><c:out value="${error}"/>
</font>
</b>
</c:forEach>
</spring:bind>
</c:if>
</form>
</div>
</body>
</html>
item.result.jsp ,
Code:
<!-- display search results -->
<c:if test="${!empty Items}" >
<br>
<b><spring:message code="search.result"/></b>:<br><br>
<simple:google modelName="Items" pagingWidth="5"/>
<table border="1">
<tr>
<th width="10%"><spring:message code="serial.no"/></th>
<th width="10%"><spring:message code="version"/></th>
<th width="10%"><simple:sorting code="system.id" sortColumn="id"/></th>
<th width="55%"><simple:sorting code="item.barcode" sortColumn="barcode"/></th>
<th width="15%"><spring:message code="action"/></th>
</tr>
<c:forEach var="item" items="${Items._resultset}" varStatus="status">
<tr>
<td><c:out value="${status.count + param['s_page']*param['s_pageSize']}"/></td>
<td><c:out value="${item.version}"/></th>
<td><c:out value="${item.id}"/></td>
<td><c:out value="${item.barcode}" escapeXml='false'/></td>
<td><a href='<c:url value="/admin/itemEditForm.htm"/>?<simple:sortlinkg idValue="${item.id}"/>'>
<spring:message code="edit"/></a>|
<a href='<c:url value="/admin/itemDeleteAfterSearch.htm"/>?<simple:sortlinkg idValue="${item.id}"/>'>
<spring:message code="delete"/></a>
</td>
</tr>
</c:forEach>
</table>
<br>
</c:if>
almost the same for other related classes .
Remark :
1. as usual , fast code.
2. l used to do the items search by using ,
Code:
Collection findItemsByShelfMark(String shelfMark);
Collection findItemsByLastCheckin(Date lastcheckin);
Collection findItemsByTitle(String title);
Collection findItemsByClassMark(String classMark);
Collection findItemsByIsbn(String isbn);
Collection findItemsByItemDuration(String itemduration);
Collection findItemsByItemStatus(String itemstatus);
Collection findItemsByItemType(String itemtype);
Collection findItemsByLocation(String location);
now no more.
3. for hibernate dynamic query , we can refer to
(a) "hibernate in action"(HIA) pg 276-279 , the last example even more powerful --->
" we can combine User properties and Item properties in the same search:" quoted from HIA.
(b) Hibernate Criteria API: Multi-Criteria Search Made Easy from devx . http://www.devx.com/Java/Article/28754
4. Some extra codes above are use to block "direct url writing the paging link" , just to make the paging more stable.
5. All codes above done in Hibernate 3.1 and spring 1.2.3
6. All parameters use to do paging have been changed to s_xxx , with prefix "s_" , to avoid conflict with other request parameters , for example , SWF or Spring use prefix "_xxx" very often.
7. it seem that we have to make a lot of commands if we use this paging pattern. (ItemCommand , BookCommand , ...etc ..)
Question : any other better way ?
8. performance not tested.
9. Reminder : have to set a StringTrimmerEditor("xxx",true) to make it work correctly.
Code:
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) {
...
binder.registerCustomEditor(String.class, null,
new StringTrimmerEditor("\r\n\f", true));
...
}
of course , set it to be false still working , but may be not what you want.You can check the output sql query string from hibernate(hibernate.show_sql=true).
l really love simple thing ...
, hibernate dynamic query is simple.
moon