// $HeadURL$
// $Id$
//
// Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College.
//
// Screensaver is an open-source project developed by the ICCB-L and NSRB labs
// at Harvard Medical School. This software is distributed under the terms of
// the GNU General Public License.
package edu.harvard.med.screensaver.ui.arch.datatable.model;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;
import org.apache.log4j.Logger;
import edu.harvard.med.screensaver.db.SortDirection;
import edu.harvard.med.screensaver.db.datafetcher.DataFetcher;
import edu.harvard.med.screensaver.ui.arch.datatable.DataTableModelType;
import edu.harvard.med.screensaver.util.ValueReference;
/**
* JSF DataModel class that supports virtual paging (i.e., on-demand fetching of
* row data).
* <p>
* Note that DataModel's wrappedData property is not supported, as virtual
* paging implies that the underlying data cannot (always) be made fully
* available.
* <p>
* @param <K> the type of the key used to identify a row of data
* @param <R> the data type containing the data displayed across each row.
* @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a>
* @author <a mailto="john_sullivan@hms.harvard.edu">John Sullivan</a>
*/
public abstract class VirtualPagingDataModel<K,R> extends DataTableModel<R>
{
private static Logger log = Logger.getLogger(VirtualPagingDataModel.class);
private static final int MAX_CACHE_SIZE_ROWS = 1 << 10;
protected DataFetcher<R,K,?> _dataFetcher;
protected ValueReference<Integer> _rowsToFetch;
protected int _rowIndex = -1;
protected List<K> _sortedKeys;
protected SortDirection _sortDirection;
protected LinkedHashMap<K,R> _fetchedRows = new LinkedHashMap<K,R>(MAX_CACHE_SIZE_ROWS, 0.75F, true /* ordered by access */) {
private static final long serialVersionUID = 1L;
protected boolean removeEldestEntry(Map.Entry<K,R> eldest) {
return _fetchedRows.size() >= MAX_CACHE_SIZE_ROWS;
}
};
protected VirtualPagingDataModel() {}
public VirtualPagingDataModel(DataFetcher<R,K,?> dataFetcher,
ValueReference<Integer> rowsToFetch)
{
_dataFetcher = dataFetcher;
_rowsToFetch = rowsToFetch;
}
// public methods
@Override
public DataTableModelType getModelType()
{
return DataTableModelType.VIRTUAL_PAGING;
}
/**
* Subclass should call this method to determine how many rows of data need to
* be fetched in order to populate the visible rows of the data table.
*/
public int getRowsToFetch()
{
return _rowsToFetch.value();
}
@Override
public int getRowIndex()
{
return _rowIndex;
}
@Override
public int getRowCount()
{
return getFilteredAndSortedKeys().size();
}
@Override
public boolean isRowAvailable()
{
return _rowIndex < getRowCount() && _rowIndex >= 0;
}
@Override
public void setRowIndex(int rowIndex)
{
_rowIndex = rowIndex;
}
@Override
public R getRowData()
{
if (!isRowAvailable()) {
return null;
}
doFetchIfNecessary();
return _fetchedRows.get(getFilteredAndSortedKeys().get(getSortIndex(_rowIndex)));
}
@Override
final public Object getWrappedData()
{
throw new UnsupportedOperationException("virtual paging data model cannot provide an object representing the full underlying dataset");
}
@Override
final public void setWrappedData(Object data)
{
throw new UnsupportedOperationException("virtual paging data model cannot be provided an object representing the full underlying dataset");
}
// private methods
private int getSortIndex(int rowIndex)
{
if (_sortDirection == SortDirection.ASCENDING) {
return rowIndex;
}
return (getRowCount() - rowIndex) - 1;
}
private List<K> getFilteredAndSortedKeys()
{
if (_sortedKeys == null) {
_sortedKeys = _dataFetcher.findAllKeys();
}
return _sortedKeys;
}
private void doFetchIfNecessary()
{
if (isRowAvailable()) {
if (!_fetchedRows.containsKey(getFilteredAndSortedKeys().get(getSortIndex(_rowIndex)))) {
Set<K> keysToFetch = getUnfetchedKeysBatch();
if (log.isDebugEnabled()) {
log.debug("need to fetch " + keysToFetch.size() + " rows");
}
Map<K,R> data = _dataFetcher.fetchData(keysToFetch);
cacheFetchedData(data);
}
}
}
private Set<K> getUnfetchedKeysBatch()
{
Set<K> keys = new HashSet<K>();
int from = _rowIndex;
int to = Math.min(_rowIndex + getRowsToFetch(), getRowCount());
for (int i = from; i < to; ++i) {
K key = getFilteredAndSortedKeys().get(getSortIndex(i));
if (!_fetchedRows.containsKey(key)) {
keys.add(key);
}
}
return keys;
}
private void cacheFetchedData(Map<K,R> fetchedData)
{
assert fetchedData.size() <= MAX_CACHE_SIZE_ROWS : "number of new data rows are larger than cache size";
_fetchedRows.putAll(fetchedData);
if (log.isDebugEnabled()) {
log.debug("cached " + fetchedData.size() + " rows; new cache size = " + _fetchedRows.size());
}
assert _fetchedRows.size() <= MAX_CACHE_SIZE_ROWS;
}
private static final Logger iterLog = Logger.getLogger(VirtualPagingDataModel.VirtualPagingIterator.class);
private final class VirtualPagingIterator implements Iterator<R>
{
private static final int FETCH_SIZE = MAX_CACHE_SIZE_ROWS;
private BlockingQueue<R> _queue;
private Thread _fetcherThread;
private int _remaining;
private class DataFetcherTask implements Runnable
{
@Override
public void run()
{
Iterator<List<K>> keyBatchIterator = Iterators.partition(_sortedKeys.iterator(), FETCH_SIZE);
while (keyBatchIterator.hasNext()) {
List<K> keyBatch = keyBatchIterator.next();
iterLog.debug("fetching next batch");
Map<K,R> data = _dataFetcher.fetchData(Sets.newHashSet(keyBatch));
for (K k : keyBatch) {
try {
_queue.put(data.get(k));
}
catch (InterruptedException e) {
iterLog.error("fetcher thread interrupted: " + e);
return;
}
}
iterLog.debug("done fetching next batch");
}
iterLog.debug("data fetcher task is done");
}
}
protected VirtualPagingIterator()
{
_queue = new LinkedBlockingQueue<R>(FETCH_SIZE);
_remaining = _sortedKeys.size();
_fetcherThread = new Thread(new DataFetcherTask());
_fetcherThread.start();
}
@Override
public boolean hasNext()
{
return _remaining > 0;
}
@Override
public R next()
{
try {
if (!hasNext()) {
return null; // note: this avoids the possibility of this method never returning, due to an exhausted queue
}
R element = _queue.take();
--_remaining;
return element;
}
catch (InterruptedException e) {
log.error("iterator.next() interrupted: " + e);
return null;
}
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<R> iterator()
{
return new VirtualPagingIterator();
}
}