/* * 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; } }