/*
* RHQ Management Platform
* Copyright (C) 2009-2010 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.core.gui.table.model;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.gui.table.bean.AbstractPagedDataUIBean;
import javax.faces.model.DataModel;
/**
* @author Ian Springer
* @author Joseph Marques
*
* @param <T> the data object type
*/
public abstract class PagedListDataModel<T> extends DataModel {
private final Log log = LogFactory.getLog(PagedListDataModel.class);
private int currentRowIndex;
private PageList<T> pageList;
private AbstractPagedDataUIBean pagedDataBean;
/**
* Create a data model that pages through the data set, showing the specified number of rows on each page.
*/
public PagedListDataModel(AbstractPagedDataUIBean pagedDataBean) {
super();
this.currentRowIndex = -1;
this.pagedDataBean = pagedDataBean;
}
/**
* Not used in this class; data is fetched via a callback to the {@link #getDataPage()} 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 getCurrentPage().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> getCurrentPage() {
// ensure page exists - first time going to this view
if (this.pageList == null) {
this.pageList = getDataPage();
} else {
PageControl currentPageControl = this.pagedDataBean.getPageControl();
PageControl lastUsedPageControl = this.pageList.getPageControl();
if (!currentPageControl.equals(lastUsedPageControl)) {
// One or more paging/sorting settings have been updated - we need to load a new page.
this.pageList = getDataPage();
}
}
return this.pageList;
}
/**
* Return the object corresponding to the current {@link #getRowIndex()}. If the PageList object currently cached
* doesn't include that index then {@link #getDataPage()} 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() {
getCurrentPage();
if (!isRowAvailable()) {
/*
* 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.
*/
// Reset back to the default page control, which will also persist the default page control.
this.pagedDataBean.resetPageControl();
getCurrentPage();
// Tell the framework to start back at the first row in the page.
this.currentRowIndex = 0;
}
int pageIndex = this.currentRowIndex - getPageControl().getStartRow();
return this.pageList.get(pageIndex);
}
@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()}.
*
* @return <code>true</code> if the row is available
*/
@Override
public boolean isRowAvailable() {
getCurrentPage();
return (this.pageList != null &&
this.currentRowIndex >= 0 &&
this.currentRowIndex < pageList.getTotalSize());
}
private PageList<T> getDataPage() {
long start = System.currentTimeMillis();
PageControl pageControl = this.pagedDataBean.getPageControl();
PageList<T> results = fetchPageGuarded(pageControl);
if (log.isDebugEnabled()) {
long time = System.currentTimeMillis() - start;
log.debug("Fetch time was [" + time + "]ms for " + pageControl);
if (time > 2000L) {
log.debug("Slow loading page " + pageControl);
}
}
return results;
}
@NotNull
private PageList<T> fetchPageGuarded(PageControl pageControl) {
PageList<T> results = null;
boolean tryQueryAgain = false;
try {
if (pageControl.getPageSize() == PageControl.SIZE_UNLIMITED && pageControl.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");
}
pageControl.setPageNumber(0);
}
// try the data fetch with the potentially changed (and persisted) PageControl object
if (log.isTraceEnabled()) {
log.trace("Fetching page using: " + pageControl);
}
results = fetchPage(pageControl);
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() != 0 && results.getTotalSize() <= pageControl.getStartRow()) ||
(results.isEmpty() && pageControl.getPageNumber() != 0)) {
if (log.isTraceEnabled()) {
if (results.getTotalSize() != 0 && results.getTotalSize() <= pageControl.getStartRow()) {
//log.trace(pageControlView + ": Results size[" + results.getTotalSize()
// + "] was less than PageControl startRow[" + pageControl.getStartRow() + "]");
} else {
//log.trace(pageControlView + ": Results were empty, but pageNumber was non-zero");
}
}
pageControl.reset();
if (log.isTraceEnabled()) {
//log.trace(pageControlView + ": resetting to " + pageControl);
}
tryQueryAgain = true;
}
} 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).
*/
pageControl.reset();
if (log.isTraceEnabled()) {
//log.trace(pageControlView + ": Received error[" + t.getMessage() + "], resetting to " + pageControl);
}
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(pageControl);
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;
}
@NotNull
public PageControl getPageControl() {
return this.pagedDataBean.getPageControl();
}
public void setPageControl(PageControl pageControl) {
this.pagedDataBean.setPageControl(pageControl);
}
/**
* Method which must be implemented in cooperation with the managed bean class to fetch data on demand.
*
* @param pageControl 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 pageControl);
/**
* TODO
*/
public void reset() {
this.pageList = null;
}
}