/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * LazyLoadingTable.java * Created: Nov 12, 2003 * By: Kevin Sit */ package org.openquark.gems.client.valueentry; import java.awt.Container; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.util.ArrayList; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.table.TableModel; /** * This class extends from the standard table to provide the ability to load rows * on demand. When the user clicks on the scroll bar, the table determines if * it needs to fetch data from the provider to fulfill the request. This table * is useful when displaying large sequential data set. * * @author ksit */ public class LazyLoadingTable extends JTable { private static final long serialVersionUID = -1117367700079761737L; /** * This event listener listens for value adjustment events posted by the vertical * scroll bar, computes a list of visible rows and invokes methods in the table * to load the rows that are not cached. */ private class VerticalScrollBarAdjustmentListener implements AdjustmentListener { /* (non-Javadoc) * @see java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event.AdjustmentEvent) */ public void adjustmentValueChanged(AdjustmentEvent e) { int value = e.getValue(); int rowHeight = getRowHeight(); if (rowHeight > 0) { int lowerBound = value / rowHeight; int upperBound = lowerBound + getPageSize(); loadRowsFromProvider(lowerBound, upperBound); } } } /** * The row provider of this table. */ protected LazyLoadingTableRowProvider provider; /** * A flag used to check if the initialize() method is called. */ private boolean initializeOnce; /** * When this flag is set, it implies that the data set has been exhausted. */ private boolean exhaustedDataSet; /** * The number of row(s) that is/are already loaded from the provider. */ private int loadedRowCount; public LazyLoadingTable(LazyLoadingTableRowProvider listener) { this.provider = listener; //initialize(); } /* (non-Javadoc) * @see javax.swing.JTable#setModel(javax.swing.table.TableModel) */ @Override public void setModel(TableModel dataModel) { if (!(dataModel instanceof LazyLoadingTableModel)) { throw new IllegalArgumentException(); } super.setModel(dataModel); } /* (non-Javadoc) * @see java.awt.Component#doLayout() */ @Override public void doLayout() { // TODO this might be a bad hack: we cannot get the page size until we // know the actual height of the view port. However, we are not // going to get it until the component tree is revalidated. So, I put // this block in here to initialize the table (and populate the table // with the first page of data) when the page size is known (and // initialize() should return true. if (!initializeOnce) { initializeOnce = initialize(); } super.doLayout(); } /** * Get the table model of this table. * @return LazyLoadingTableModel */ public LazyLoadingTableModel getLazyLoadingTableModel() { return (LazyLoadingTableModel) getModel(); } /** * Returns the number of row(s) that the table should look ahead (and cache). * By default, this method returns twice the page size. * <p> * Note: the sum of the look ahead size and the page size has to be greater * than the visible row count. Otherwise, the scroll bar will not appear. * @return int */ public int getLookAheadSize() { // the default behaviour is to look ahead for 2 pages return 2 * getPageSize(); } /** * Returns the number of row(s) that the viewport can display. * <p> * Note: the sum of the look ahead size and the page size has to be greater * than the visible row count. Otherwise, the scroll bar will not appear. * * If the view port is not yet visible, the number of rows that can be displayed * will not be known. In this case we use the maximum viewport size to ensure * enough data is loaded. * * @return int */ public int getPageSize() { int visibleRowCount = getVisibleRowCount(); if (visibleRowCount > 0) { return visibleRowCount; } //No rows are visible yet - use the maximum viewport size instead JScrollPane scrollPane = getScrollPane(); if (scrollPane != null) { return scrollPane.getViewport().getMaximumSize().height / getRowHeight(); } return 512; } /** * Returns the maximum number of row(s) that is/are visible to the user. * This method uses the parent viewport to determine the height of the * visible area. If there is no viewport defined, then this method * will use the table's height to determine the visible area. This method * should be called after the component tree is validated, otherwise, * the visible area will always be evaluated to zero. * @return int */ public int getVisibleRowCount() { int visibleHeight = -1; // if the table is embedded within a scroll pane, then get the visible area // from the viewport JScrollPane scrollPane = getScrollPane(); if (scrollPane != null) { visibleHeight = scrollPane.getViewport().getHeight(); } // otherwise, use the table's height instead if (visibleHeight <= 0) { visibleHeight = getHeight(); } // get the visible row count by dividing the visible drawing area height by the // row height return visibleHeight / getRowHeight(); } /* (non-Javadoc) * @see javax.swing.JTable#createDefaultDataModel() */ @Override protected TableModel createDefaultDataModel() { return new LazyLoadingTableModel(); } /** * Initializes the table and populates the table with one page of data * (plus any look ahead data). This method should be called once only, * after the component tree is validated. * @return boolean */ protected boolean initialize() { JScrollPane sp = getScrollPane(); if (sp != null) { // force the scroll pane to calculate the scroll bar width/height based on // the actual size of the table; we have use set the preferred viewport size // to null because JTable uses some dummy value by default setPreferredScrollableViewportSize(null); // insert a listener to listen for adjustment changes originated by the // vertical scroll bar of the scroll pane JScrollBar sb = sp.getVerticalScrollBar(); if (sb != null) { sb.addAdjustmentListener(new VerticalScrollBarAdjustmentListener()); } // reset the exhaustedDataSet flag because we are starting from the beginning exhaustedDataSet = false; // populate the table with one page of data, plus any look ahead data loadRowsFromProvider(0, getPageSize()); return true; } return false; } /** * Given a set/range of rows to be loaded, this method attempts to load the rows * that are not loaded already. If the given set does not intersect with * the current set of rows that are already loaded, then this method will * also attempt to load the set of rows between the two sets. This method is * also aware of the look ahead value returned by the <code>getLookAheadSize()</code> * method. If the given range causes the number of cached row to drop * below the look ahead size, then this method will also attempt to load * extra rows from the provider in order to bring the cache size back up. * @param lowerBound inclusive * @param upperBound exclusive */ protected void loadRowsFromProvider(int lowerBound, int upperBound) { // if we have exhausted the data set already, then no loaded is required if (exhaustedDataSet) { return; } // swap the boundary values so that upper bound is always larger than the // lower bound if (lowerBound > upperBound) { int n = lowerBound; lowerBound = upperBound; upperBound = n; } // compute the number of row(s) that are already available (i.e. no // fetching required) and the number of row(s) that are missing from // the local data model int pageSize = getPageSize(); int lookAheadSize = getLookAheadSize(); int availableRowCount = Math.min(loadedRowCount - lowerBound, pageSize); int lookAheadRowCount = Math.min(lookAheadSize - (loadedRowCount - upperBound), lookAheadSize); int fetchRowCount = (upperBound - lowerBound) - availableRowCount + lookAheadRowCount; if (fetchRowCount > 0) { ArrayList<Object[]> rowList = new ArrayList<Object[]>(fetchRowCount); for (int i = 0; i < fetchRowCount; i++, loadedRowCount++) { if (provider.hasRow(loadedRowCount)) { rowList.add(provider.loadRow(loadedRowCount)); } else { // we have exhausted the data set, assuming that the data set is // sequential and it does not have gaps in between: i.e. it is not possible // to have hasRow(i) returns true, hasRow(i+x) returns false, and // hasRow(i+x+1) returns true where x > 0. exhaustedDataSet = true; loadedRowCount++; break; } } Object[][] rowArray = new Object[rowList.size()][]; rowList.toArray(rowArray); getLazyLoadingTableModel().addAllRows(rowArray); } } /** * Returns the scroll pane, if there is any, that contains this table. * @return JScrollPane */ protected JScrollPane getScrollPane() { Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane)gp; // Make certain we are the viewPort's view and not, for // example, the rowHeaderView of the scrollPane - // an implementor of fixed columns might do this. JViewport viewport = scrollPane.getViewport(); if (viewport != null && viewport.getView() == this) { return scrollPane; } } } return null; } }