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