/*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is the Kowari Metadata Store.
*
* The Initial Developer of the Original Code is Plugged In Software Pty
* Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions
* created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002
* Plugged In Software Pty Ltd. All Rights Reserved.
*
* Contributor(s): N/A.
*
* [NOTE: The text of this Exhibit A may differ slightly from the text
* of the notices in the Source Code files of the Original Code. You
* should use the text of this Exhibit A rather than the text found in the
* Original Code Source Code for Your Modifications.]
*
*/
package org.mulgara.server.rmi;
// Java 2 standard packages
import java.rmi.RemoteException;
// Third party packages
import org.apache.log4j.*;
// Local packages
import org.mulgara.query.AbstractAnswer;
import org.mulgara.query.Answer;
import org.mulgara.query.TuplesException;
import org.mulgara.query.Variable;
import org.mulgara.server.rmi.AnswerPage;
import org.mulgara.util.StackTrace;
/**
* Wrap a {@link RemoteAnswer} to make it into an {@link Answer}.
*
* @created 2004-03-17
* @author <a href="http://staff.pisoftware.com/raboczi">Simon Raboczi</a>
* @version $Revision: 1.9 $
* @modified $Date: 2005/01/05 04:59:02 $ by $Author: newmana $
* @company <A href="mailto:info@PIsoftware.com">Plugged In Software</A>
* @copyright ©2004 <a href="http://www.pisoftware.com/">Plugged In
* Software Pty Ltd</a>
* @licence <a href="{@docRoot}/../../LICENCE">Mozilla Public License v1.1</a>
*/
class RemoteAnswerWrapperAnswer extends AbstractAnswer implements Answer, Cloneable {
/** logger */
private static final Logger logger =
Logger.getLogger(AnswerWrapperRemoteAnswer.class.getName());
/**
* Default timeout period to wait for a new page.
*/
public static final String PREFETCH_TIMEOUT_PROPERTY = "mulgara.rmi.pagetimeout";
/**
* Default timeout period to wait for a new page.
*/
public static final int DEFAULT_PREFETCH_TIMEOUT = 60000;
/**
* The wrapped instance.
*/
private RemoteAnswer remoteAnswer;
/**
* Immutable variables.
*/
private final Variable[] variables;
/**
* The current page of data to return.
*/
private AnswerPage currentPage = null;
/**
* The timeout period to wait for a new page.
*/
private int timeout;
/**
* The thread used for prefetching the next page of answers.
*/
private PrefetchThread prefetchThread;
/**
* Optimisation to prevent dropping of the first page when beforeFirst is called.
*/
private boolean onFirstPage = false;
/**
* Keeps the stack trace of where the answer was originally closed.
*/
private StackTrace closedTrace;
private boolean closed = false;
//
// Constructor
//
/**
* Wrap a {@link RemoteAnswer} to make it into an {@link Answer}.
*
* @param remoteAnswer the instance to wrap
* @throws IllegalArgumentException if <var>remoteAnswer</var> is
* <code>null</code>
*/
RemoteAnswerWrapperAnswer(RemoteAnswer remoteAnswer) throws RemoteException {
// Validate "remoteAnswer" parameter
if (remoteAnswer == null) {
throw new IllegalArgumentException("Null \"remoteAnswer\" parameter");
}
// Initialize the wrapped instance
this.remoteAnswer = remoteAnswer;
variables = remoteAnswer.getVariables();
currentPage = null;
onFirstPage = false;
prefetchThread = null;
// Initialize the page timeout
timeout = Integer.getInteger(PREFETCH_TIMEOUT_PROPERTY,
DEFAULT_PREFETCH_TIMEOUT).intValue();
}
//
// Methods implementing Answer
//
/**
* Clone the current RemoteAnswer wrapper, and increment the reference count to the RemoteAnswer.
*
* @return The new RemoteAnswer wrapper. This refers to the same remote answer as the original.
*/
public Object clone() {
try {
// protect the RMI threading model
waitForPrefetchThread();
RemoteAnswerWrapperAnswer a = (RemoteAnswerWrapperAnswer)super.clone();
a.remoteAnswer = this.remoteAnswer.remoteClone();
a.currentPage = null;
a.onFirstPage = false;
a.prefetchThread = null;
return a;
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException rex) {
throw new RuntimeException("Clone failed on server", rex);
}
}
/**
* Return the object at the given index.
*
* @param column column numbering starts from zero
* @return the value at the given index
* @throws SQLException on failure
* @throws TuplesException EXCEPTION TO DO
*/
public Object getObject(int column) throws TuplesException {
assert currentPage != null;
try {
Object obj = currentPage.getObjectFromPage(column);
// If this is a remote answer then wrap it
if (obj instanceof RemoteAnswer) {
obj = new RemoteAnswerWrapperAnswer( (RemoteAnswer) obj);
}
return obj;
} catch (RemoteException e) {
throw new TuplesException("Unable to get column " + column, e);
}
}
/**
* Return the object at the given column name.
*
* @param columnName the index of the object to retrieve
* @return the value at the given index
* @throws SQLException on failure
* @throws TuplesException EXCEPTION TO DO
*/
public Object getObject(String columnName) throws TuplesException {
assert currentPage != null;
try {
Object obj = currentPage.getObjectFromPage(columnName);
// If this is a remote answer then wrap it
if (obj instanceof RemoteAnswer) {
obj = new RemoteAnswerWrapperAnswer( (RemoteAnswer) obj);
}
return obj;
} catch (RemoteException e) {
throw new TuplesException("Unable to get column \"" + columnName + "\"", e);
}
}
//
// Methods implementing Cursor
//
/**
* Reset to iterate through every single element.
*
* @throws TuplesException EXCEPTION TO DO
*/
public synchronized void beforeFirst() throws TuplesException {
try {
waitForPrefetchThread();
if (onFirstPage) {
currentPage.beforeFirstInPage();
} else {
currentPage = remoteAnswer.beforeFirstAndInitPage();
// make onFirstPage false if the page is invalid
onFirstPage = (currentPage != null);
// Abandon the last prefetched page, and start a new prefetch thread
prefetchThread = new PrefetchThread();
}
} catch (RemoteException er) {
logger.warn("RemoteException thrown in beforeFirst", er);
throw new TuplesException("Couldn't reset remote cursor", er);
} catch (RMITimeoutException te) {
throw new TuplesException("Couldn't reset remote cursor", te);
}
}
/**
* Free resources associated with this instance.
*
* @throws TuplesException EXCEPTION TO DO
*/
public void close() throws TuplesException {
//ensure the prefetchThread is not fetching next page
if (prefetchThread != null) {
try {
prefetchThread.join(timeout);
} catch (InterruptedException ie) {
logger.info("Join on prefetchThread interrupted.", ie);
}
if (!prefetchThread.hasFinished()) {
logger.warn("No RMI data returned within " + timeout + "ms while closing");
}
prefetchThread = null;
}
if (closed) {
logger.warn("Was already closed.");
if (closedTrace != null) logger.debug("Originally closed at: " + closedTrace);
throw new TuplesException("Attempting to close answer twice.\n" + new StackTrace());
}
closed = true;
if (logger.isDebugEnabled()) closedTrace = new StackTrace();
// no more references left, close the remote object
try {
remoteAnswer.close();
currentPage = null;
onFirstPage = false;
} catch (RemoteException e) {
throw new TuplesException("Couldn't close remote cursor", e);
}
// set the remote answer to null for the sake of the finalize method below
remoteAnswer = null;
}
/**
* Find the index of a variable.
*
* @param column PARAMETER TO DO
* @return The ColumnIndex value
* @throws TuplesException EXCEPTION TO DO
*/
public int getColumnIndex(Variable column) throws TuplesException {
if (variables == null) {
throw new TuplesException("No columns in Answer");
}
// Validate "column" parameter
if (column == null) {
throw new IllegalArgumentException("Null \"column\" parameter");
}
// Look for the requested variable in the "variables" array
for (int i = 0; i < this.getNumberOfVariables(); i++) {
if (column.equals(variables[i])) {
return i;
}
}
// Couldn't find the requested variable
throw new TuplesException("No such column " + column);
}
/**
* Returns the number of variables (columns).
*
* @return the number of variables (columns)
*/
public int getNumberOfVariables() {
int noVars = 0;
if (variables != null) {
noVars = variables.length;
}
return noVars;
}
/**
* The variables bound and their default collation order. The array returned
* by this method should be treated as if its contents were immutable, even
* though Java won't enforce this. If the elements of the array are modified,
* there may be side effects on the past and future clones of the tuples it
* was obtained from.
*
* @return the {@link Variable}s bound within this answer.
*/
public Variable[] getVariables() {
return variables;
}
/**
* Tests whether this is a unit-valued answer. A unit answer appended to
* something yields the unit answer. A unit answer joined to something yields
* the same something. Notionally, the unit answer has zero columns and one
* row.
*
* @return The Unconstrained value
* @throws TuplesException EXCEPTION TO DO
*/
public boolean isUnconstrained() throws TuplesException {
try {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
return remoteAnswer.isUnconstrained();
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException e) {
throw new TuplesException("Can't test for unconstrained", e);
}
}
/**
* This method returns the number of rows which this instance contains.
*
* @return an upper bound on the number of rows that this instance contains.
* @throws TuplesException EXCEPTION TO DO
*/
public long getRowCount() throws TuplesException {
try {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
return remoteAnswer.getRowCount();
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException e) {
throw new TuplesException("Can't get remote row count", e);
}
}
public long getRowUpperBound() throws TuplesException {
try {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
return remoteAnswer.getRowUpperBound();
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException e) {
throw new TuplesException("Can't get remote row upper bound", e);
}
}
public long getRowExpectedCount() throws TuplesException {
try {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
return remoteAnswer.getRowExpectedCount();
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException e) {
throw new TuplesException("Can't get remote expected row count", e);
}
}
public int getRowCardinality() throws TuplesException {
try {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
return remoteAnswer.getRowCardinality();
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException e) {
throw new TuplesException("Can't get remote row cardinality", e);
}
}
public boolean isEmpty() throws TuplesException {
try {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
return remoteAnswer.isEmpty();
} catch (RMITimeoutException rmie) {
throw new RuntimeException("Timeout waiting on server", rmie);
} catch (RemoteException e) {
throw new TuplesException("Can't get remote isEmpty", e);
}
}
/**
* Move to the next row.
*
* If no such row exists, return <code>false<code> and the current row
* becomes unspecified. The current row is unspecified when an
* instance is created. To specify the current row, the
* {@link #beforeFirst()} method must be invoked.
* Behaviour is undefined if next() is called again after it returns <code>false</code>.
*
* @return whether a subsequent row exists
* @throws IllegalStateException if the current row is unspecified.
* @throws TuplesException EXCEPTION TO DO
*/
public boolean next() throws TuplesException {
try {
// Check if there is a current page, if there is then move to the next and check for validity
if (currentPage == null || !currentPage.nextInPage()) {
// no valid page. Get the next page and initialise.
// moving onto the first page only if there was no current page
onFirstPage = (currentPage == null);
// move to the next page
if (currentPage != null && currentPage.isLastPage()) {
currentPage = null;
} else {
currentPage = nextPage();
}
// Move to the first item in the returned page
if (currentPage != null) {
boolean test = currentPage.nextInPage();
assert test || ! (currentPage instanceof AnswerPageImpl);
// instances of AnswerPageImpl should be null if it contains no valid rows
} else {
// no valid page: if it was the first page, then turn the flag off
onFirstPage = false;
}
}
// Return true if we have a current valid page (page can't be finished at this point)
return currentPage != null;
} catch (RemoteException e) {
throw new TuplesException("Can't advance remote cursor", e);
} catch (RMITimeoutException te) {
throw new TuplesException("Can't get to next page of answers", te);
}
}
/**
* Retrieves the next page from remoteAnswer, and starts a new thread to prefetch the
* page that comes in next.
*
* @return The next page from the answer.
*/
protected AnswerPage nextPage() throws RMITimeoutException, TuplesException, RemoteException {
waitForPrefetchThread();
assert prefetchThread == null || prefetchThread.hasFinished();
AnswerPage page = null;
if (prefetchThread != null) {
// a finished thread exists
page = prefetchThread.getPendingPage();
} else {
// no old thread
page = remoteAnswer.nextPage();
}
// launch new prefetch thread
prefetchThread = new PrefetchThread();
return page;
}
/**
* Wait on the prefetched page if it is already coming in.
*
* @throws RMITimeoutException
*/
private void waitForPrefetchThread() throws RMITimeoutException {
if (prefetchThread != null) {
try {
prefetchThread.join(timeout);
} catch (InterruptedException ie) {
// Not concerned about interruptions, only in finishing
}
if (!prefetchThread.hasFinished()) {
// abandon the joining thread
prefetchThread = null;
throw new RMITimeoutException("No data returned within " + timeout + "ms");
}
}
}
/**
* Clean up the remote object if it has not already been done.
*/
protected void finalize() throws Throwable {
try {
if (remoteAnswer != null) remoteAnswer.close();
} finally {
remoteAnswer = null;
super.finalize();
}
}
/**
* Thread to prefetch the next page from the remoteAnswer
*/
private class PrefetchThread
extends Thread {
/** The page to be fetched */
private AnswerPage page;
/** Flag indicating that this thread has completed its task */
private boolean finished;
/** Stack Trace for client-side invokation */
private final StackTrace caller;
/**
* Main constructor. Starts the current thread.
*/
public PrefetchThread() {
this.caller = logger.isDebugEnabled() ? new StackTrace() : null;
page = null;
finished = false;
start();
}
/**
* The main code in this thread. Simply requests a new page.
*/
public void run() {
try {
page = remoteAnswer.nextPage();
finished = true;
} catch (Exception e) {
// finished will never be set
// log exception and include the stack trace that created this Thread.
logger.warn("Exception thrown in prefetchThread.");
if (caller != null && logger.isDebugEnabled()) logger.debug("Prefetch caller: " + caller);
logger.warn("Caused by", e);
}
}
/**
* Indicates if the thread has run to successful completion
*
* @return <code>true</code> if the thread has finished.
*/
public boolean hasFinished() {
return finished;
}
/**
* Returned the prefetched page.
*
* @return The page that was fetched by this thread.
*/
public AnswerPage getPendingPage() {
if (!finished) {
throw new IllegalStateException(
"Unable to request pages until the prefetch thread has completed");
}
return page;
}
}
}