/* * 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, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.gui.model; import org.ajax4jsf.model.DataVisitor; import org.ajax4jsf.model.ExtendedDataModel; import org.ajax4jsf.model.Range; import org.ajax4jsf.model.SequenceRange; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.domain.util.OrderingField; import org.rhq.core.domain.util.PageControl; import org.rhq.core.domain.util.PageList; import org.rhq.core.domain.util.PageOrdering; import org.richfaces.model.FilterField; import org.richfaces.model.Modifiable; import org.richfaces.model.Ordering; import org.richfaces.model.SortField2; import javax.el.Expression; import javax.faces.context.FacesContext; import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; /** * @author Ian Springer * @author Joseph Marques * * @param <T> the type of domain object (e.g. org.rhq.core.domain.Resource) that this data model represents */ public class PagedDataModel<T> extends ExtendedDataModel implements Modifiable { private final Log log = LogFactory.getLog(this.getClass()); protected Object currentRowKey; protected PageList<T> currentPage; protected LinkedHashMap<Object, T> currentPageDataByKey; private PagedDataProvider<T> dataProvider; /** * the default pagination and sort settings */ private PageControl defaultPageControl; private SequenceRange currentSequenceRange; private List<OrderingField> currentOrderingFields = Collections.emptyList(); private boolean sortRequested; public PagedDataModel(PagedDataProvider<T> dataProvider) { this.dataProvider = dataProvider; // TODO: get the default page control from some managed bean this.defaultPageControl = new PageControl(0, 15); } /** * @see org.ajax4jsf.model.ExtendedDataModel#getRowKey() */ @Override public Object getRowKey() { return this.currentRowKey; } /** * @see org.ajax4jsf.model.ExtendedDataModel#setRowKey(Object) */ @SuppressWarnings("unchecked") @Override public void setRowKey(Object key) { this.currentRowKey = key; } /** * @see org.ajax4jsf.model.ExtendedDataModel#walk(javax.faces.context.FacesContext, * org.ajax4jsf.model.DataVisitor, org.ajax4jsf.model.Range, * Object) */ @Override public void walk(FacesContext facesContext, DataVisitor dataVisitor, Range range, Object argument) throws IOException { SequenceRange sequenceRange = (SequenceRange) range; boolean newPageRequested = (this.currentSequenceRange == null || sequenceRange.getFirstRow() != this.currentSequenceRange.getFirstRow() || sequenceRange.getRows() != this.currentSequenceRange.getRows()); if (newPageRequested) { log.info("*** New page requested."); } this.currentSequenceRange = sequenceRange; if (newPageRequested || this.sortRequested) { // If this is the very first request for this data set, or if the user has specified new paging or sorting // settings, then we request data from data provider. PageControl pageControl = createPageControl(); loadDataPage(pageControl); this.sortRequested = false; } // Let the visitor pay a visit to each item in the current page. this.currentPageDataByKey is a LinkedHashMap, // so we know we'll visit the nodes in the correct order. for (Object key : this.currentPageDataByKey.keySet()) { dataVisitor.process(facesContext, key, argument); } } /** * @see org.richfaces.model.Modifiable#modify(java.util.List, java.util.List) */ public void modify(List<FilterField> filterFields, List<SortField2> sortFields) { if (!sortRequested) { List<OrderingField> orderingFields = toOrderingFields(sortFields); fixOrder(orderingFields); this.sortRequested = (!orderingFields.equals(this.currentOrderingFields)); if (this.sortRequested) { log.info("*** Sort requested - order: " + orderingFields + ", previous order: " + this.currentOrderingFields); } this.currentOrderingFields = orderingFields; } } /** * @see javax.faces.model.DataModel#isRowAvailable() */ @Override public boolean isRowAvailable() { if (this.currentRowKey == null) { return false; } if (this.currentPageDataByKey.containsKey(this.currentRowKey)) { return true; } // TODO: Should we load the current page here and look again for the current key - I don't think so. return false; } /** * @see javax.faces.model.DataModel#getRowData() */ @Override public Object getRowData() { if (this.currentRowKey == null) { // TODO: Is it right to return null here? return null; } T dataObject = this.currentPageDataByKey.get(this.currentRowKey); if (dataObject == null) { loadDataPage(this.defaultPageControl); } return dataObject; } /** * @see javax.faces.model.DataModel#getRowCount() */ @Override public int getRowCount() { if (this.currentPage == null) { loadDataPage(this.defaultPageControl); } return this.currentPage.getTotalSize(); } /** * @see javax.faces.model.DataModel#setRowIndex(int) */ @Override public void setRowIndex(final int rowIndex) { throw new UnsupportedOperationException("This method is not called by the RichFaces rich:extendedDataTable component."); } /** * @see javax.faces.model.DataModel#setWrappedData(Object) */ @Override public void setWrappedData(final Object data) { throw new UnsupportedOperationException("This method is not called by the RichFaces rich:extendedDataTable component."); } /** * @see javax.faces.model.DataModel#getRowIndex() */ @Override public int getRowIndex() { throw new UnsupportedOperationException("This method is not called by the rich:extendedDataTable component."); } /** * @see javax.faces.model.DataModel#getWrappedData() */ @Override public Object getWrappedData() { throw new UnsupportedOperationException("This method is not called by the rich:extendedDataTable component."); } private PageControl createPageControl() { int pageSize = this.currentSequenceRange.getRows(); int pageNumber = this.currentSequenceRange.getFirstRow() / pageSize; PageControl pageControl = new PageControl(pageNumber, pageSize); pageControl.getOrderingFields().addAll(this.currentOrderingFields); return pageControl; } /** * Convert RichFaces SortField2s to RHQ OrderingFields. * * @param sortFields the SortField2s to be converted * * @return the equivalent RHQ OrderingFields */ private List<OrderingField> toOrderingFields(List<SortField2> sortFields) { if (sortFields == null) { sortFields = Collections.emptyList(); } List<OrderingField> orderingFields = new ArrayList<OrderingField>(sortFields.size()); for (SortField2 sortField : sortFields) { Expression expression = sortField.getExpression(); String expressionString = expression.getExpressionString(); String field; if (expression.isLiteralText()) { field = expressionString; } else { field = expressionString.replaceAll("[#|$]\\{", "").replaceAll("\\}", ""); } Ordering ordering = sortField.getOrdering(); PageOrdering pageOrdering = (ordering == Ordering.ASCENDING) ? PageOrdering.ASC : PageOrdering.DESC; OrderingField orderingField = new OrderingField(field, pageOrdering); orderingFields.add(orderingField); } return orderingFields; } private void loadDataPage(PageControl pageControl) { this.currentPage = getDataPage(pageControl); this.currentPageDataByKey = new LinkedHashMap<Object, T>(this.currentPage.size()); for (T dataObject : this.currentPage) { Object key = getPrimaryKey(dataObject); this.currentPageDataByKey.put(key, dataObject); } } /** * Return the domain object's primary key - typically an Integer. Pretty much all RHQ domain objects use 'id' as the * field name for the primary key, so provide a default implementation that grabs the 'id' field to make things as * easy as possible for subclasses. * * @param object a domain object * @return U the domain object's primary key - typically an Integer */ protected Object getPrimaryKey(T object) { Method method; try { method = object.getClass().getMethod("getId"); } catch (NoSuchMethodException e) { throw new IllegalStateException(object.getClass() + " does not define a public getId() method."); } try { return method.invoke(object); } catch (Exception e) { throw new IllegalStateException("Failed to invoke getId() on " + object + ".", e); } } private PageList<T> getDataPage(PageControl pageControl) { PageList<T> results = null; boolean tryQueryAgain = false; try { if (log.isTraceEnabled()) { //log.trace(pageControlView + ": " + pageControl); } 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); //setPageControl(pageControl); } // try the data fetch with the potentially changed (and persisted) PageControl object results = this.dataProvider.getDataPage(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() <= pageControl.getStartRow() || (results.isEmpty() && pageControl.getPageNumber() != 0)) { if (log.isTraceEnabled()) { if (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(); // this will reset to page 1, but will not reset any ordering. 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(); // this will reset to page 1, but will not reset any ordering. 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 = this.dataProvider.getDataPage(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; } private void fixOrder(List<OrderingField> orderingFields) { Collections.reverse(orderingFields); if (this.currentOrderingFields != null && orderingFields.size() > 1 && orderingFields.size() == this.currentOrderingFields.size()) { for (int i = 1, sortFieldsSize = orderingFields.size(); i < sortFieldsSize; i++) { OrderingField orderingField = orderingFields.get(i); OrderingField currentOrderingField = this.currentOrderingFields.get(i); if (orderingField.getField().equals(currentOrderingField.getField()) && orderingField.getOrdering() != currentOrderingField.getOrdering()) { orderingFields.remove(i); orderingFields.add(0, orderingField); } } } } }