/*
* Copyright 2008 Fedora Commons, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mulgara.resolver.distributed.remote;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.util.AbstractSet;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import org.apache.log4j.Logger;
/**
* Represents an iterable object on a remote system as a local Set.
* Created at the server side, and sent across the network.
* @copyright 2007 <a href="http://www.fedora-commons.org/">Fedora Commons</a>
*/
public class SetProxy<E extends Serializable> extends AbstractSet<E> implements Serializable {
/** Serial ID for versioning. */
private static final long serialVersionUID = -8343698708605937025L;
/** Logger. */
private static final Logger logger = Logger.getLogger(RemotePagerImpl.class.getName());
/** Stores the currently running iterator. */
private static Object currentIterator = null;
/** A pager for returning sequential pages of a remote collection. */
private final RemotePager<E> remotePager;
/** The size of the remote collection. */
private final int cachedSize;
/**
* Creates a new proxy for a remote collection, meeting the Set interface.
* @param remotePager A device for sending data from the remote collection one page at a time.
*/
public SetProxy(RemotePager<E> remotePager) {
this.remotePager = remotePager;
// local call for size
try {
cachedSize = remotePager.size();
} catch (RemoteException re) {
throw new IllegalStateException("The proxy should be instantiated on the host side");
}
}
/**
* Returns the number of elements in the underlying collection.
* @return The size of the collection.
*/
public int size() {
return cachedSize;
}
/**
* Returns an iterator which will access all the remote data.
* NOTE: The current implementation allows only one iterator to be active at a time!
* @return A new iterator for the remote data.
*/
public Iterator<E> iterator() {
return new PagedIterator();
}
/**
* An iterator class for traversing remote data. Network activity is reduced by moving
* data in large pages at a time.
*/
private class PagedIterator implements Iterator<E> {
/** A thread for managing bringing the pages over the network. */
private Pager pager;
/** The most recent page of data. */
private E[] currentPage;
/** The current position in the current page of data. */
int index;
/**
* Create a new iterator for traversing pages of data.
*/
public PagedIterator() {
currentPage = null;
index = 0;
currentIterator = this;
logger.info("Starting pager");
pager = new Pager();
currentPage = pager.nextPage();
logger.info("Started pager");
}
/**
* Remove the current element from the data. Unsupported.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Queries the data to check if more data exists. Should not need to block.
* @return <code>true</code> if more data exists.
* @throws ConcurrentModificationException If more than one iterator is active.
*/
public boolean hasNext() {
logger.info("called SetProxy$Iterator.hasNext()");
testState();
if (currentPage != null && index < currentPage.length) return true;
return currentPage != null;
}
/**
* Returns the next element of the data. Will block until data is available.
* @return The next item of data in sequence.
* @throws ConcurrentModificationException If more than one iterator is active.
*/
public E next() {
logger.info("called SetProxy$Iterator.next()");
testState();
logger.info("Accessing element " + index + " of " + currentPage.length);
if (currentPage != null && index < currentPage.length) return nextPageElement();
return nextPageElement();
}
/**
* Gets the next element out of the current page.
* @return The next element from the current page.
*/
private E nextPageElement() {
logger.info("Getting next page element");
E element = currentPage[index++];
if (index == currentPage.length) updatePage();
return element;
}
/**
* Moves to the next page, if another page is available.
*/
private void updatePage() {
logger.info("Moving to next page");
currentPage = pager.nextPage();
index = 0;
}
/**
* Check that this is the only iterator being accessed at the moment.
* @throws ConcurrentModificationException If this iterator is being accessed
* after a new iterator has been created.
*/
private void testState() {
if (currentIterator != this) {
throw new ConcurrentModificationException("Unable to use more than one remote iterator on the set");
}
}
/**
* Private thread for getting the next page in the background.
*/
private class Pager extends Thread {
/** The maximum number of pages that may be queued. */
private final int maxPages = Config.getMaxPages();
/** Maximum time to wait for a page to arrive, in milliseconds. */
private final long timeout = Config.getTimeout();
/** Indicates that the thread has finished. */
private boolean complete;
/** The retrieved pages. */
private Queue<E[]> retrievedPages;
/** Stores exception when one occurs. */
private PagerException lastException;
/**
* Initialize and start the thread.
* Main thread.
*/
public Pager() {
lastException = null;
retrievedPages = new LinkedList<E[]>();
try {
logger.info("Getting first page");
E[] page = remotePager.firstPage();
if (page != null) {
logger.info("Got data in first page: size=" + page.length);
retrievedPages.add(page);
complete = false;
start();
} else logger.info("Empty initial page");
} catch (RemoteException re) {
throw new PagerException("Unable to get the first page", re);
}
}
/**
* Checks if the thread is active. Main thread.
* @return <code>false</code> if the thread is still running, <code>true</code> when complete.
*/
@SuppressWarnings("unused")
public boolean isComplete() {
if (lastException != null) throw lastException;
return complete;
}
/**
* Pick up all the pages.
* Runs in the background Paging thread.
*/
public void run() {
try {
while (true) {
synchronized (retrievedPages) {
while (retrievedPages.size() >= maxPages) {
try {
logger.info("Waiting for queue to empty. Currently at: " + retrievedPages.size());
retrievedPages.wait();
} catch (InterruptedException ie) { }
}
}
E[] page = remotePager.nextPage();
if (page == null) {
logger.info("Got final page");
break;
}
logger.info("Got next page. size=" + page.length);
synchronized (retrievedPages) {
retrievedPages.add(page);
logger.info("Queue now at " + retrievedPages.size() + " pages");
}
synchronized (this) {
this.notify();
}
}
} catch (RemoteException re) {
logger.error("Error retrieving remote data", re);
lastException = new PagerException("Unable to retrieve page", re);
}
complete = true;
}
/**
* Get the next page, if available. The page will be an array of the configured length,
* or shorter if it is the last page. If there is no more data, then <code>null</code>
* will be returned.
* Runs in the Main thread.
* @return The next page of data, or <code>null</code> if no more data exists.
*/
public E[] nextPage() {
logger.info("Request for next page");
if (lastException != null) throw lastException;
E[] page;
long startTime = System.currentTimeMillis();
while (true) {
synchronized (retrievedPages) {
int oldSize = retrievedPages.size();
logger.info("Queue has " + oldSize + " pages");
page = retrievedPages.poll();
if (oldSize >= maxPages) retrievedPages.notify();
logger.info("page @" + page);
}
long waitTime = timeout + startTime - System.currentTimeMillis();
if (waitTime <= 0) throw new PagerException("Timed out waiting for page");
try {
synchronized (this) {
if (page == null && !complete) {
logger.info("Waiting for more pages to arrive");
this.wait(waitTime);
} else break;
}
} catch (InterruptedException ie) { }
if (System.currentTimeMillis() - startTime >= timeout) throw new PagerException("Timed out waiting for page");
}
logger.info("Returning page = " + page);
return page;
}
}
}
/** Exception class for paging. Must be runtime so it can be thrown through Set interface. */
@SuppressWarnings("serial")
public static class PagerException extends RuntimeException {
public PagerException() { }
public PagerException(String message) { super(message); }
public PagerException(String message, RemoteException cause) { super(message, cause); }
public PagerException(RemoteException cause) { super(cause); }
}
}