/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.gui.common.paging;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.model.DataModel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.gui.util.FacesContextUtility;
import org.rhq.enterprise.gui.common.framework.PagedDataTableUIBean;
import org.rhq.enterprise.gui.legacy.WebUser;
import org.rhq.enterprise.gui.util.EnterpriseFacesContextUtility;
import org.rhq.enterprise.server.authz.PermissionException;
import org.rhq.enterprise.server.util.HibernatePerformanceMonitor;
/**
* <p>A special type of JSF DataModel to allow a table and datascroller to page through a large set of data without
* having to hold the entire set of data in memory at once.</p>
*
* <p>Any time a managed bean wants to avoid holding an entire dataset, the managed bean should declare an inner class
* which extends this class and implements the fetchData method. This method is called as needed when the table requires
* data that isn't available in the current data page held by this object.</p>
*
* <p>This does require the managed bean (and in general the business method that the managed bean uses) to provide the
* data wrapped in a PageList object that provides info on the full size of the dataset.</p>
*
* <p>Adapted from - http://wiki.apache.org/myfaces/WorkingWithLargeTables</p>
*
* @param <T> the data type to be stored in the dataset
* @author Joseph Marques
*/
public abstract class PagedListDataModel<T> extends DataModel {
private final Log log = LogFactory.getLog(PagedListDataModel.class);
private int currentRowIndex;
private PageControlView pageControlView;
private PageList<T> pageList;
private String beanName;
/**
* Create a datamodel that pages through the data showing the specified number of rows on each page.
*
* @param pageSize the total number of pages in the full dataset
*/
public PagedListDataModel(PageControlView view, String beanName) {
super();
this.currentRowIndex = -1;
this.pageControlView = view;
this.pageList = null;
this.beanName = beanName;
}
/**
* Not used in this class; data is fetched via a callback to the {@link #getDataPage(PageControl)} method rather
* than by explicitly assigning a list.
*
* @param o unused
*
* @throws UnsupportedOperationException thrown when this method is called
*/
@Override
public void setWrappedData(Object o) {
throw new UnsupportedOperationException("setWrappedData");
}
@Override
public int getRowIndex() {
return this.currentRowIndex;
}
/**
* Specify what the "current row" within the dataset is. Note that the UIData component will repeatedly call this
* method followed by getRowData to obtain the objects to render in the table.
*
* @param index current row index, indexes start at 0
*/
@Override
public void setRowIndex(int index) {
this.currentRowIndex = index;
}
/**
* Return the total number of rows of data available (not just the number of rows in the current page!).
*
* @return number of rows in the full dataset
*/
@Override
public int getRowCount() {
return getPage().getTotalSize();
}
/**
* Return a PageList object; if one is not currently available then fetch one. Note that this doesn't ensure that
* the PageList returned includes the current {@link #getRowIndex()} row; see {@link #getRowData()}.
*
* @return the current page
*/
private PageList<T> getPage() {
// ensure page exists - first time going to this view
if (pageList == null) {
log.trace("pageList was null, will get PageControl then load the page");
PageControl pageControl = getPageControl();
PageList<T> results = getDataPage(pageControl);
pageList = results;
log.trace("pageList was loaded, found " + (pageList == null ? "null" : pageList.size() + " result items"));
}
return pageList;
}
/**
* Return the object corresponding to the current {@link #getRowIndex()}. If the PageList object currently cached
* doesn't include that index then {@link #getDataPage(PageControl)} is called to retrieve the appropriate page.
*
* @return the row data that corresponds to {@link #getRowIndex()}
*
* @throws IllegalArgumentException if the {@link #getRowIndex()} is outside the range of the dataset size
*/
@Override
public Object getRowData() {
if (this.currentRowIndex < 0) {
throw new IllegalArgumentException("Invalid rowIndex for PagedListDataModel; not within page");
}
PageControl pageControl = getPageControl();
int startRow = pageControl.getStartRow();
int endRow;
if (pageControl.getPageSize() == PageControl.SIZE_UNLIMITED) {
endRow = Integer.MAX_VALUE;
} else {
int nRows = pageControl.getPageSize();
endRow = startRow + nRows - 1;
}
if (log.isTraceEnabled()) {
log.trace("getRowData(" + currentRowIndex + "): startRow=" + startRow + ", endRow=" + endRow);
}
// paging backwards - will we ever get in this if-statement if pageControl.getPageSize == SIZE_UNLIMITED?
if (currentRowIndex < startRow) {
int rowsBack = startRow - currentRowIndex;
int pagesBack = (int) Math.ceil(rowsBack / (double) pageControl.getPageSize());
int newPage = pageControl.getPageNumber() - pagesBack;
if (log.isTraceEnabled()) {
log.trace("paging down by " + rowsBack + " rows / " + pagesBack + " pages, new page is " + newPage);
}
if (newPage < 0) {
newPage = 0;
log.trace("newPage was negative, setting page to 0");
}
pageControl.setPageNumber(newPage);
pageList = getDataPage(pageControl);
startRow = pageControl.getStartRow();
}
// paging forwards
else if (currentRowIndex > endRow) {
int rowsForward = currentRowIndex - endRow;
int pagesForward = (int) Math.ceil(rowsForward / (double) pageControl.getPageSize());
int newPage = pageControl.getPageNumber() + pagesForward;
if (log.isTraceEnabled()) {
log.trace("paging up by " + rowsForward + " rows / " + pagesForward + " pages, new page is " + newPage);
}
pageControl.setPageNumber(newPage);
pageList = getDataPage(pageControl);
startRow = pageControl.getStartRow();
}
/*
* March 11, 2009 - the only currently known way this can fail is if the countQuery returned 0 but the
* actual data query returned nothing; generally, this is a programming error, but it's possible that
* the facelet changed (new columns shown, other columns removed) or the query itself changed (and perhaps
* the sortable columns are different); in either case, let's be paranoid and try it all over again with
* a default PageControl object for this PageControlView
*/
int getIndex = currentRowIndex - startRow;
if (log.isTraceEnabled()) {
log.trace("currentRowIndex=" + currentRowIndex + ", startRow=" + startRow + ", getIndex=" + getIndex
+ ", pageListSize=" + pageList.size());
}
if (getIndex < 0 || getIndex >= pageList.size()) {
log.trace("getIndex is out of pageList's bounds, getting default page control");
// getting the default will repersist the new PageControl too
pageControl = getDefaultPageControl();
pageList = getDataPage(pageControl);
// pageControl startRow should now be zero
this.currentRowIndex = 0; // and tell the framework to start back at 0
getIndex = 0; // now the getIndex should be 0
if (log.isTraceEnabled()) {
log.trace("currentRowIndex=" + currentRowIndex + ", startRow=" + startRow + ", getIndex=" + getIndex
+ ", pageListSize=" + pageList.size());
}
}
if (log.isTraceEnabled() && pageList.get(getIndex) == null) {
log.trace("Data item at position " + getIndex + " was null");
}
return pageList.get(getIndex);
}
@Override
public Object getWrappedData() {
return pageList;
}
/**
* Return <code>true</code> if the {@link #getRowIndex()} value is currently set to a value that matches some
* element in the dataset. Note that it may match a row that is not in the currently cached PageList; if so then
* when {@link #getRowData()} is called the required PageList will be fetched by calling
* {@link #getDataPage(PageControl)}.
*
* @return <code>true</code> if the row is available
*/
@Override
public boolean isRowAvailable() {
PageList<T> page = getPage();
if (page == null) {
return false;
}
int rowIndex = getRowIndex();
if (rowIndex < 0) {
return false;
} else if (rowIndex >= page.getTotalSize()) {
return false;
} else {
return true;
}
}
private PagedDataTableUIBean getPagedDataTableUIBean() {
FacesContext facesContext = FacesContextUtility.getFacesContext();
ExternalContext externalContext = facesContext.getExternalContext();
PagedDataTableUIBean result = (PagedDataTableUIBean) externalContext.getRequestMap().get(beanName);
if (result == null) {
result = (PagedDataTableUIBean) externalContext.getSessionMap().get(beanName);
}
return result;
}
public PageControl getPageControl() {
WebUser user = EnterpriseFacesContextUtility.getWebUser();
PageControl pageControl = getPagedDataTableUIBean().getPageControl(user, pageControlView);
if (log.isTraceEnabled()) {
log.trace("getPageControl() -->" + pageControl);
}
return pageControl;
}
public PageControl getDefaultPageControl() {
WebUser user = EnterpriseFacesContextUtility.getWebUser();
return getPagedDataTableUIBean().getDefaultPageControl(user, pageControlView);
}
public void setPageControl(PageControl pageControl) {
if (log.isTraceEnabled()) {
log.trace("setPageControl(" + pageControl + ")");
}
WebUser user = EnterpriseFacesContextUtility.getWebUser();
getPagedDataTableUIBean().setPageControl(user, pageControlView, pageControl);
}
public PageList<T> getDataPage(PageControl pc) {
long start = System.currentTimeMillis();
long monitorId = HibernatePerformanceMonitor.get().start();
PageList<T> results = fetchPageGuarded(pc);
HibernatePerformanceMonitor.get().stop(monitorId, pageControlView.toString());
if (log.isDebugEnabled()) {
long time = System.currentTimeMillis() - start;
log.debug("Fetch time was [" + time + "]ms for " + pageControlView);
if (time > 2000L) {
log.debug("Slow loading page " + pageControlView);
}
}
return results;
}
private PageList<T> fetchPageGuarded(PageControl pc) {
PageList<T> results = null;
boolean tryQueryAgain = false;
try {
if (log.isTraceEnabled()) {
log.trace(pageControlView + ": " + pc);
}
if (pc.getPageSize() == PageControl.SIZE_UNLIMITED && pc.getPageNumber() != 0) {
/*
* user is trying to get all of the results (SIZE_UNLIMITED), but not starting
* on the first page. while this is technically allowable, it generally doesn't
* make all that much sense and was most likely due to a mistake upstream in the
* usage of the pagination / sorting framework.
*/
if (log.isTraceEnabled()) {
log.trace(pageControlView + ": Forcing UNLIMITED PageControl's pageNumber to 0");
}
pc.setPageNumber(0);
setPageControl(pc);
}
// try the data fetch with the potentially changed (and persisted) PageControl object
results = fetchPage(pc);
if (log.isTraceEnabled()) {
log.trace(pageControlView + ": Successfully fetched page (first time)");
}
/*
* do the results make sense? there are certain times when no exception will be thrown but the
* user interface won't be properly updated because of the multi-user environment. if one user
* is looking at some data set while another user deletes that entire data set, the current user
* upon next sort or pagination action should realize that no results exist for his current page
* and the view should be rendered to reflect that. however, due to some defensive coding in the
* RF components, the DataTable component does not see this change. so, we have to explicitly
* update the page control to get the view consistent with the backend once again.
*/
if (results.getTotalSize() < pc.getStartRow() || (results.isEmpty() && pc.getPageNumber() != 0)) {
if (log.isTraceEnabled()) {
if (results.getTotalSize() < pc.getStartRow()) {
log.trace(pageControlView + ": Results size[" + results.getTotalSize()
+ "] was less than PageControl startRow[" + pc.getStartRow() + "]");
} else {
log.trace(pageControlView + ": Results were empty, but pageNumber was non-zero");
}
}
resetToDefaults(pc);
if (log.isTraceEnabled()) {
log.trace(pageControlView + ": resetting to " + pc);
}
tryQueryAgain = true;
}
} catch (PermissionException pe) {
throw pe; // don't try to reload the data page upon authorization failures, just let it bubble up
} catch (Throwable t) {
/*
* known issues during pagination:
*
* 1) IndexOutOfBoundsException - trying to access a non-existent page
* 2) QuerySyntaxException - when the token passed by the SortableColumnHeaderListener does not
* match some alias on the underlying query that fetches the results
*
* but let's be extra careful and catch Throwable so as to handle any other exceptional case
* we've yet to uncover. however, we still want to return value data to the user, so let's
* try the query once again; this time, we want the first page and will not specify any explicit
* ordering (though the underlying SLSB may add a default ordering downstream).
*/
resetToDefaults(pc);
log.error(pageControlView + ": Received error[" + t.getMessage() + "], resetting to " + pc);
tryQueryAgain = true;
}
// round 2 should be guaranteed because of use of defaultPageControl
if (tryQueryAgain) {
if (log.isTraceEnabled()) {
log.trace(pageControlView + ": Trying query again");
}
try {
results = fetchPage(pc);
if (log.isTraceEnabled()) {
log.trace(pageControlView + ": Successfully fetched page (second time)");
}
} catch (Throwable t) {
log.error("Could not retrieve collection for " + pageControlView, t);
}
}
if (results == null) {
results = new PageList<T>();
}
return results;
}
private void resetToDefaults(PageControl pc) {
pc.reset();
setPageControl(pc);
}
/**
* Method which must be implemented in cooperation with the managed bean class to fetch data on demand.
*
* @param pc information such as the first row of data to be fetched, the number of rows of data to be fetched and
* sorting data
*
* @return the data page with the rows in memory
*/
public abstract PageList<T> fetchPage(PageControl pc);
public PageControlView getPageControlView() {
return pageControlView;
}
}