/*
* Copyright (C) 2012-2015 Glencoe Software, 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 as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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 for more details.
*
* You should have received a copy of the GNU 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 omero.cmd;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import omero.ServerError;
import Ice.Current;
/**
*
* Callback servant used to wait until a HandlePrx would
* return non-null on getReponse. The server will notify
* of completion to prevent constantly polling on
* getResponse. Subclasses can override methods for handling
* based on the completion status.
*
* Example usage:
* <pre>
* cb = new CmdCallbackI(client, handle);
* response = null;
* while (response == null) {
* response = cb.block(500);
* }
*
* // or
*
* response = cb.loop(5, 500);
* </pre>
*
* Subclasses which depend on the proper ordering of either initialization
* or calls to {@link #onFinished(Response, Status, Current)} should make
* use of the {@link #initializationDone()} and {@link #waitOnInitialization()},
* or the {@link #onFinishedDone()} and {@link #waitOnFinishedDone()} methods.
*
* @author Josh Moore, josh at glencoesoftware.com
* @since Beta4.4
*
* @see #initializationDone()
* @see #onFinishedDone()
*/
public class CmdCallbackI extends _CmdCallbackDisp {
private static class State {
final Response rsp;
final Status status;
State(Response rsp, Status status) {
this.rsp = rsp;
this.status = status;
}
}
private static final long serialVersionUID = 1L;
private final Ice.ObjectAdapter adapter;
private final Ice.Identity id;
/**
* Latch which is released once {@link #finished(Response, Status, Current)} is
* called. Other methods will block on this value.
*/
private final CountDownLatch latch = new CountDownLatch(1);
/**
* @see #initializationDone()
* @see #waitOnInitialization()
*/
private final CountDownLatch isInitialized = new CountDownLatch(1);
/**
* @see #onFinishedDone()
* @see #waitOnFinishedDone()
*/
private final CountDownLatch isOnFinishedDone = new CountDownLatch(1);
private final AtomicReference<State> state = new AtomicReference<State>(
new State(null, null));
/**
* Proxy passed to this instance on creation. Can be used by subclasses
* freely. The object will not be nulled, but may be closed server-side.
*/
protected final HandlePrx handle;
public CmdCallbackI(omero.client client, HandlePrx handle)
throws ServerError {
this(client.getAdapter(), client.getCategory(), handle);
}
public CmdCallbackI(CmdCallbackI ccb) throws ServerError {
this(ccb.adapter, ccb.id.category, ccb.handle);
}
public CmdCallbackI(Ice.ObjectAdapter adapter, String category,
HandlePrx handle)
throws ServerError {
this.adapter = adapter;
this.handle = handle;
this.id = new Ice.Identity();
this.id.name = UUID.randomUUID().toString();
this.id.category = category;
Ice.ObjectPrx prx = adapter.add(this, id);
CmdCallbackPrx cb = CmdCallbackPrxHelper.uncheckedCast(prx);
handle.addCallback(cb);
initialPoll();
}
//
// Subclass initialization
//
/**
* Called at the end of construction to check a race condition.
*
* If {@link HandlePrx} finishes its execution before the
* {@link CmdCallbackPrx} has been sent set via addCallback,
* then there's a chance that this implementation will never
* receive a call to finished, leading to perceived hangs.
*
* By default, this method starts a background thread and
* calls {@link #poll()}. An {@link Ice.ObjectNotExistException}
* implies that another caller has already closed the
* {@link HandlePrx}.
*/
protected void initialPoll() {
// Now check just in case the process exited VERY quickly
new Thread() {
public void run() {
try {
poll();
} catch (Exception e) {
// don't throw any exceptions, e. g. if the handle
// has already been closed
onFinished(null, null, null);
}
}
}.start();
}
/**
* Subclasses which must perform their own initialization before
* {@link #onFinished(Response, Status, Current)} is called should
* call {@link #initializationDone()} once that setup is complete.
*/
protected void initializationDone() {
isInitialized.countDown();
}
/**
* Subclasses which must perform their own initialization before
* {@link #onFinished(Response, Status, Current)} is called should
* call {@link #waitOnInitialization()} before accessing any initialized
* state.
*/
protected void waitOnInitialization() {
try {
isInitialized.await();
} catch (InterruptedException ie) {
// pass
}
}
protected void onFinishedDone() {
isOnFinishedDone.countDown();
}
protected void waitOnFinishedDone() {
try {
isOnFinishedDone.await();
} catch (InterruptedException ie) {
// pass
}
}
//
// Local invocations
//
/**
* Returns possibly null Response value. If null, then neither has
* the remote server nor the local poll method called finish with
* non-null values.
* @return the response, may be {@code null}
*/
public Response getResponse() {
return state.get().rsp;
}
/**
* Returns possibly null Status value. If null, then neither has
* the remote server nor the local poll method called finish with
* non-null values.
* @return the status, may be {@code null}
*/
public Status getStatus() {
return state.get().status;
}
protected Status getStatusOrThrow() {
Status s = getStatus();
if (s == null) {
throw new omero.ClientError("Status not present!");
}
return s;
}
/**
* Returns whether Status::CANCELLED is contained in
* the flags variable of the Status instance. If no
* Status is available, a ClientError will be thrown.
* @return if {@link omero.cmd.State#CANCELLED} has been flagged
*/
public boolean isCancelled() {
Status s = getStatusOrThrow();
return s.flags.contains(omero.cmd.State.CANCELLED);
}
/**
* Returns whether Status::FAILURE is contained in
* the flags variable of the Status instance. If no
* Status is available, a ClientError will be thrown.
* @return if {@link omero.cmd.State#FAILURE} has been flagged
*/
public boolean isFailure() {
Status s = getStatusOrThrow();
return s.flags.contains(omero.cmd.State.FAILURE);
}
/**
* Calls block(long) "loops" number of times with the "ms"
* argument. This means the total wait time for the action to occur
* is: loops X ms. Sensible values might be 10 loops for 500 ms, or
* 5 seconds.
*
* @param loops Number of times to call block(long)
* @param ms Number of milliseconds to pass to block(long
* @return the response
* @throws InterruptedException if the thread was interrupted
* @throws omero.LockTimeout if block(long) does not return
* a non-null value after loops calls.
*/
public Response loop(int loops, long ms) throws InterruptedException,
omero.LockTimeout {
int count = 0;
boolean found = false;
while (count < loops) {
count++;
found = block(ms);
if (found) {
break;
}
}
if (found) {
return getResponse();
} else {
double waited = (ms/1000.0) * loops;
throw new omero.LockTimeout(null, null,
String.format("Command unfinished after %s seconds",
waited), 10000, (int) waited);
}
}
/**
* Blocks for the given number of milliseconds unless
* {@link #finished(Response, Status, Current)} has been called in which case
* it returns immediately with true. If false is returned, then the timeout
* was reached.
*
* @param ms Milliseconds which this method should block for.
* @return if the the thread finished before the timeout was reached
* @throws InterruptedException if the thread was interrupted
*/
public boolean block(long ms) throws InterruptedException {
return latch.await(ms, TimeUnit.MILLISECONDS);
}
//
// Remote invocations
//
/**
* Calls {@link HandlePrx#getResponse} in order to check for a non-null
* value. If so, {@link Handle#getStatus} is also called, and the two
* non-null values are passed to
* {@link #finished(Response, Status, Current)}. This should typically
* not be used. Instead, favor the use of block and loop.
*
*/
public void poll() {
Response rsp = handle.getResponse();
if (rsp != null) {
Status s = handle.getStatus();
finished(rsp, s, null); // Only time that current should be null.
}
}
/**
* Called periodically by the server to signal that processing is
* moving forward. Default implementation does nothing.
*/
public void step(int complete, int total, Current __current) {
// no-op
}
/**
* Called when the command has completed.
*/
public final void finished(Response rsp, Status status, Current __current) {
state.set(new State(rsp, status));
latch.countDown();
onFinished(rsp, status, __current);
}
/**
* Method intended to be overridden by subclasses. Default logic does
* nothing.
* @param rsp the response
* @param status the status
* @param __current regarding the current method invocation
*/
public void onFinished(Response rsp, Status status, Current __current) {
// no-op
}
/**
* First removes self from the adapter so as to no longer receive
* notifications, and the calls close on the remote handle if requested.
* @param closeHandle if the handle should be closed
*/
public void close(boolean closeHandle) {
adapter.remove(id); // OK ADAPTER USAGE
if (closeHandle) {
handle.close();
}
}
}