/* * RHQ Management Platform * Copyright (C) 2005-2011 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.domain.util; import java.io.Serializable; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Used to pass information on pagination and sorting to data lookup methods. * {@link org.rhq.core.domain.server.PersistenceUtility} provides several methods * that can be called to apply PageControls to various types of queries. * * @author Greg Hinkle * @author Joseph Marques */ public class PageControl implements Serializable, Cloneable { private static final long serialVersionUID = 1L; private static final int MAX_ORDERING_FIELD_COUNT = 3; public static final int SIZE_UNLIMITED = -1; public static final int SIZE_MAX = 100; private int pageNumber = 0; private int pageSize = PageControl.SIZE_MAX; private Integer firstRecord; private LinkedList<OrderingField> orderingFields; public PageControl() { this.orderingFields = new LinkedList<OrderingField>(); } public PageControl(int pageNumber, int pageSize) { this(); this.pageNumber = pageNumber; this.pageSize = pageSize; } public static PageControl getExplicitPageControl(int firstRecord, int recordCount) { PageControl pc = new PageControl(-1, recordCount); pc.firstRecord = firstRecord; return pc; } public PageControl(int pageNumber, int pageSize, OrderingField... orderingFields) { this(pageNumber, pageSize); for (OrderingField orderingField : orderingFields) { // null fields are equivalent to not setting the ordering field at all if (orderingField.getField() != null) { this.orderingFields.add(orderingField); } } } public static PageControl getUnlimitedInstance() { return new PageControl(0, SIZE_UNLIMITED); } public static PageControl getSingleRowInstance() { return new PageControl(0, 1); } /** * Equivalent to initDefaultOrderingField(defaultField, PageOrdering.ASC). * * @param defaultField * @see #initDefaultOrderingField(String, PageOrdering) */ public void initDefaultOrderingField(String defaultField) { initDefaultOrderingField(defaultField, PageOrdering.ASC); } /** * Sets initial sort. If sorting is already defined this call will have no effect. * * @param defaultField * @param defaultPageOrdering */ public void initDefaultOrderingField(String defaultField, PageOrdering defaultPageOrdering) { if (orderingFields.size() > 0) { return; } addDefaultOrderingField(defaultField, defaultPageOrdering); } /** * Equivalent to addDefaultOrderingField(defaultField, PageOrdering.ASC). * * @param defaultField * @see #addDefaultOrderingField(String, PageOrdering) */ public void addDefaultOrderingField(String defaultField) { addDefaultOrderingField(defaultField, PageOrdering.ASC); } /** * Add a default ordering field. If the maximum number of sort fields (currently 3) are already * defined this call will have no effect. If the field is already a sort field this call will have no * effect. Otherwise, the ordering field will be appended to the existing ordering fields. * * @param defaultField * @param defaultPageOrdering */ public void addDefaultOrderingField(String defaultField, PageOrdering defaultPageOrdering) { if (orderingFields.size() >= MAX_ORDERING_FIELD_COUNT) { return; // only need to add defaults if there are less than 3 sort orders } for (OrderingField ordering : orderingFields) { if (ordering.getField().equals(defaultField)) { /* * return immediately, since we've apparently already added this sort field to the * list of OrderingFields; UI actions, in particular the integration of sort columns * on tabular displays with this PageControl object, will dictate whether or not the * PageOrdering will be reversed, but that logic occurs elsewhere */ return; } } orderingFields.add(new OrderingField(defaultField, defaultPageOrdering)); } /** * @return The current page number (0-based) */ public int getPageNumber() { return pageNumber; } public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageNumber = (pageSize != 0 && pageSize != SIZE_UNLIMITED) ? (getStartRow() / pageSize) : 0; this.pageSize = pageSize; } public Integer getFirstRecord() { return firstRecord; } public void setFirstRecord(Integer firstRecord) { this.firstRecord = firstRecord; if (this.firstRecord != null) { this.pageNumber = -1; } } public PageOrdering getPrimarySortOrder() { OrderingField primaryOrderingField = getPrimaryOrderingField(); if (primaryOrderingField == null) { return null; } return getPrimaryOrderingField().getOrdering(); } public void setPrimarySortOrder(PageOrdering sortOrder) { OrderingField primaryOrderingField = getPrimaryOrderingField(); if (primaryOrderingField == null) { // attempting to change sortOrder when no sortColumn has been selected, ignore return; } primaryOrderingField.setOrdering(sortOrder); } public String getPrimarySortColumn() { OrderingField primaryOrderingField = getPrimaryOrderingField(); if (primaryOrderingField == null) { return null; } return primaryOrderingField.getField(); } public void setPrimarySort(String sortColumn, PageOrdering sortOrder) { OrderingField primaryOrderingField = getPrimaryOrderingField(); if (primaryOrderingField == null) { primaryOrderingField = new OrderingField(sortColumn, sortOrder); orderingFields.add(primaryOrderingField); } else { primaryOrderingField.setField(sortColumn); primaryOrderingField.setOrdering(sortOrder); } } private OrderingField getPrimaryOrderingField() { OrderingField result = null; if (orderingFields.size() != 0) { result = orderingFields.get(0); } return result; } public OrderingField[] getOrderingFieldsAsArray() { return orderingFields.toArray(new OrderingField[0]); } public List<OrderingField> getOrderingFields() { return orderingFields; } public void truncateOrderingFields(int keepFieldCount) { int removeCount = orderingFields.size() - keepFieldCount; for (int i = 0; i < removeCount; i++) { orderingFields.removeLast(); } } public void removeOrderingField(String doomedField) { for (Iterator<OrderingField> i = orderingFields.iterator(); i.hasNext();) { OrderingField field = i.next(); if (field.getField().equals(doomedField)) { i.remove(); break; } } } public void sortBy(String sortField) { boolean wasAlreadySortedOn = false; for (int i = 0, sz = orderingFields.size(); i < sz; i++) { OrderingField field = orderingFields.get(i); if (sortField.equals(field.getField())) { /* * When a user clicks on some column to sort it, that column is promoted to the primary sort column, and * the rest of the orderings move down in the list */ orderingFields.remove(i); field.flipOrdering(); orderingFields.addFirst(field); wasAlreadySortedOn = true; break; } } /* * if we weren't already sorting on this field, we'll add it as the new primary sort. however, for objects with * many, many fields we want to limit the number of sorted columns, so we'll remove any elements if the list * size exceeds MAX_ORDERING_FIELD_COUNT */ if (wasAlreadySortedOn == false) { OrderingField field = new OrderingField(sortField, PageOrdering.ASC); orderingFields.addFirst(field); if (orderingFields.size() > MAX_ORDERING_FIELD_COUNT) { orderingFields.removeLast(); } } } /** * Get the index of the first item on the page as dictated by the page size and page number. * * @return the index of the starting row for the page */ public int getStartRow() { if (firstRecord != null) { return firstRecord; } else { return pageNumber * pageSize; } } public void reset() { // allow the pageSize to remain, which keeps unlimited views with unlimited paging pageNumber = 0; firstRecord = null; orderingFields = new LinkedList<OrderingField>(); } public boolean isUnlimited() { return getPageNumber() == 0 && getPageSize() == SIZE_UNLIMITED; } /** * Checks whether this page control object is consistent with the supplied collection and totalSize values. * The results (collection and totalSize) are consistent iff: * <ul> * <li>This page control "points" past the totalSize and the collection is empty,</li> * <li>or if this is an unlimited page control, the collection size is equal to * <code>totalSize - {@link #getStartRow()}</code>,</li> * <li>or if this control object points to the last page of the results then the size of the collection is equal * to the remainder of <code>totalSize / {@link #getPageSize()}</code>,</li> * <li>otherwise the collection size is equal to the {@link #getPageSize() page size}</li> * </ul> * <p/> * The primary reason why a page control would be inconsistent with the found results is the phenomenon called * "phantom read" which can happen if there was a database change between performing a query to get the collection * (which represents the paged subset limited by this page control object) and performing the count query * to get the total number of results (without paging applied). This is an unfortunate consequence of using the * {@code READ_COMMITTED} transaction isolation level for our database connection, which is needed for it being * reasonably performant. * * @param collection the collection of results * @param totalSize the total size of results * @return true if this page control object is consistent with the results or not */ public boolean isConsistentWith(Collection<?> collection, int totalSize) { int minTotalSize = getStartRow(); int pageSize = isUnlimited() ? Integer.MAX_VALUE : getPageSize(); if (totalSize < minTotalSize) { // user reading past the available number of results. this is "normal" condition caused either // by carelessness of the user or by a drastically changed number of rows since the request for the // "previous" page. return collection.isEmpty(); } int sizeDiff = totalSize - minTotalSize; // there can be 2 cases here: // 1) the total number of rows is large enough to fill the current page. // 2) we're showing the last page of the results and thus the number of results should be equal to the // number of results expected on that last page. int expectedCollectionSize = Math.min(sizeDiff, pageSize); return collection.size() == expectedCollectionSize; } /** * If you can a {@link PageList} that is inconsistent with this page control object, it is recommended you try * calling the method you obtained the page list from again. Maybe the database have "settled down" from the * activity that caused that inconsistency. * <p/> * This is a convenience function that is equivalent to calling: * <p> * <code>isConsistentWith(pageList, pageList.getTotalSize())</code> * </p> * * @see #isConsistentWith(java.util.Collection, int) * * @param pageList the page list to check the consistency with this page control * @return true if the page list is consistent, false otherwise. */ public boolean isConsistentWith(PageList<?> pageList) { return isConsistentWith(pageList, pageList.getTotalSize()); } // TODO (ips, 10/12/11): Incorporate firstRecord field into equals() and hashCode(). @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PageControl that = (PageControl) o; if (pageNumber != that.pageNumber) return false; if (pageSize != that.pageSize) return false; if (!orderingFields.equals(that.orderingFields)) return false; return true; } @Override public int hashCode() { int result = pageNumber; result = 31 * result + pageSize; result = 31 * result + orderingFields.hashCode(); return result; } @Override public String toString() { StringBuilder buf = new StringBuilder("PageControl["); if (firstRecord != null) { buf.append("firstRow=").append(firstRecord); } else { buf.append("page=").append(pageNumber); } buf.append(", size=").append(pageSize); int i = 0; if (orderingFields.size() > 0) { buf.append(", sort["); for (OrderingField orderingField : orderingFields) { if (i++ != 0) { buf.append(", "); } buf.append(orderingField.getField()).append(" ").append(orderingField.getOrdering()); } buf.append("]"); } buf.append("]"); return buf.toString(); } public Object clone() { return new PageControl(pageNumber, pageSize, getOrderingFieldsAsArray()); } }