
net.sf.eBus.client.EReplyFeed Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library 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 Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright 2015, 2016. Charles W. Rapp
// All Rights Reserved.
//
package net.sf.eBus.client;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.ERequestFeed.RequestState;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.EReplyMessage.ReplyStatus;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
/**
* {@code EReplyFeed} is the application entry point for posting
* replies to {@link ERequestMessage request messages} to
* requestors. Follow these steps to use this feed:
*
* Step 1: Implement the {@link EReplier}
* interface.
*
*
* Step 2:
* {@link #open(EReplier, EMessageKey, EFeed.FeedScope, ECondition) Open}
* a reply feed for a given {@code EReplier} instance and
* {@link EMessageKey type+topic message key}. The condition is
* optional and may be {@code null}. If provided, then only
* request messages satisfying the condition are forwarded to the
* replier.
*
*
* Step 3 (optional): Do not override
* {@link EReplier} interface methods. Instead, set callbacks
* using {@link #requestCallback(RequestCallback)} and/or
* {@link #cancelRequestCallback(CancelRequestCallback)} passing
* in Java lambda expressions.
*
*
* Step 4: {@link #advertise() Advertise} this
* replier to eBus. This allows eBus to match repliers with
* requestors.
*
*
* Step 5: Wait for
* {@link EReplier#request(EReplyFeed.ERequest) requests}
* to arrive. Keep the given {@link ERequest} instance handy
* because {@code ERequest} is used to post reply messages back
* to the requestor, not {@code EReplyFeed}.
*
*
* Step 6:
* {@link EReplyFeed.ERequest#reply(EReplyMessage) Send} one or
* more {@link EReplyMessage reply} messages back to the
* requestor.
*
*
* Step 7: When the replier is shutting down,
* {@link #unadvertise() retract} the reply advertisement and
* {@link #close() close} the feed.
*
* Example use of {@code EReplyFeed}
* import java.util.ArrayList;
import java.util.List;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EReplier;
import net.sf.eBus.client.EReplyFeed;
import net.sf.eBus.messages.EReplyMessage
import net.sf.eBus.messages.EReplyMessage.ReplyStatus; import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ERequestMessage;
Step 1: Implement EReplier interface.
public class CatalogReplier implements EReplier {
public CatalogReplier(final String subject, final FeedScope scope) {
mKey = new EMessageKey(com.acme.CatalogOrder.class, subject); mScope = scope;
mFeed = null;
mRequests = new ArrayList<>();
}
@Override
public void startup() {
try {
Step 2: Open a reply feed for reply message key.
// This advertisement has no associated ECondition; passing in null.
mFeed = EReplyFeed.open(this, mKey, mScope, null);
Step 3: EReplier interface methods overridden.
Step 4: Advertise reply feed.
mFeed.advertise();
mFeed.updateFeedState(EFeedState.UP);
} catch (IllegalArgumentException argex) {
// Advertisement failed. Place recovery code here.
}
}
Step 5: Wait for requests to arrive.
@Override
public void request(final EReplyFeed.ERequest request, final EReplyFeed feed) {
final ERequestMessage msg = request.request();
try {
mRequests.add(request);
startOrderProcessing(msg, request);
} catch (Exception jex) {
request.reply(new CatalogOrderReply(ReplyStatus.ERROR, // reply status.
jex.getMessage())); // reply reason.
}
}
@Override
public void cancelRequest(final EReplyFeed.ERequest request, final EReplyFeed feed) {
// Is this request still active? It is if the request is listed.
if (mRequests.remove(request)) {
// Yes, try to stop the request processing.
try {
// Throws an exception if the request cannot be stopped.
stopOrderProcessing(request);
} catch (Exception jex) {
// Ignore since nothing else can be done.
}
}
}
Step 6: Send one or more reply messages back to requestor.
public void orderReply(final EReplyFeed.ERequest request, final boolean status, final String reason) {
final ERequestAd ad = mRequests.get(request);
if (mRequests.contains(request) && request.isActive()) {
request.reply(new OrderReply(status, reason), ad);
// If the request processing is complete, remove the request.
if (status.isFinal()) {
mRequests.remove(request);
}
}
}
@Override
public void shutdown() {
final String subject = mKey.subject();
EReplyMessage reply;
// While eBus will does this for us, it is better to do it ourselves.
for (EReplyFeed.ERequest request : mRequests) {
reply = new EReplyMessage(subject, ReplyStatus.ERROR, "shutting down");
request.reply(reply);
}
mRequests.clear();
Step 7: When shutting down, either unadvertise or close reply feed.
if (mFeed != null) {
mFeed.close();
mFeed = null;
}
}
private final EMessageKey mKey;
private final FeedScope mScope;
private EReplyFeed mFeed;
private final List<EReplyFeed.ERequest> mRequests;
}
*
* @see EReplier
* @see ERequestor
* @see ERequestFeed
*
* @author Charles Rapp
*/
public final class EReplyFeed
extends ESingleFeed
implements IEReplyFeed
{
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code ERequest} is responsible for matching an
* {@link ERequestFeed.ERequest} with a single
* {@link EReplyFeed}. The replier uses
* {@link ERequest#reply(EReplyMessage)} to post a
* {@link EReplyMessage reply message} back to the requesting
* client. Replies cannot be sent using {@link EReplyFeed}
* nor can {@code ERequest} instances be retrieved from
* {@code EReplyFeed}. It is the application's responsibility
* to track active {@code ERequest} instances in order to
* send reply messages back to requestors.
*
* ({@code EReplyFeed} does track the active {@code ERequest}
* instances but for internal purposes only. The reason is
* due to {@code ERequest} not having the original request
* message. Without the request message, an {@code ERequest}
* object is unusable by the application.)
*
*
* A {@link ERequestor request client} does not directly
* interact with {@code ERequest} instances but with its
* {@link ERequestFeed.ERequest}. This is because there is a
* one-to-one
* relationship between {@code ERequestFeed.ERequest} and
* {@code EReplyFeed.ERequest}.
*
*
* Each request is given an identifier that is unique for the
* request lifespan. Once the request reaches the
* {@link RequestState#DONE done} or
* {@link RequestState#CANCELED canceled} state, this
* identifier is returned an identifier pool for re-use. It
* is likely that a later request will be assigned the same
* identifier.
*
*
* @see ERequestFeed
* @see ERequestFeed.ERequest
* @see EReplyFeed
* @see ERequestMessage
* @see EReplyMessage
*/
public static final class ERequest
extends ESingleFeed
{
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new request instance which links a reply and
* a request feed.
* @param reqFeed forward replies to this requestor.
* @param repFeed replies come from this feed.
* @param request the request message.
* @param cb request cancellation callback.
*/
/* package */ ERequest(final ERequestFeed.ERequest reqFeed,
final EReplyFeed repFeed,
final ERequestMessage request,
final CancelRequestCallback cb)
{
super (repFeed.mEClient,
repFeed.mScope,
repFeed.mFeedType,
repFeed.mSubject);
_requestor = reqFeed;
_replier = repFeed;
_request = request;
_cancelCallback = cb;
_requestState = RequestState.ACTIVE;
// Assume local until updated.
_remaining = 1;
} // end of ERequest(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// EFeed Abstract Method Overrides.
//
/**
* Cancels this request if still active.
*/
@Override
protected void inactivate()
{
if (_requestState == RequestState.ACTIVE)
{
state(RequestState.CANCELED);
// Post this request cancellation to the replier.
(_replier.mEClient).dispatch(
new CancelRequestTask(
this, _replier, _cancelCallback));
}
return;
} // end of inactivate()
@Override
/* package */ int updateActivation(final ClientLocation loc,
final EFeedState fs)
{
return (0);
} // end of updateActivation(ClientLocation, EFeedState)
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return ((_request.key()).toString());
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns the associated request message.
* @return request message.
*/
public ERequestMessage request()
{
return (_request);
} // end of request()
/**
* Returns the current request state.
* @return the request state.
*/
public RequestState state()
{
return (_requestState);
} // end of state()
/**
* Returns the associated reply feed.
* @return a reply feed.
*/
public EReplyFeed replier()
{
return (_replier);
} // end of replier()
/**
* Returns the associated request feed.
* @return request feed.
*/
/* package */ ERequestFeed.ERequest requestor()
{
return (_requestor);
} // end of requestor()
/**
* Returns the remote replier count for this request.
* @return number of remaining remote repliers.
*/
/* package */ int remaining()
{
return (_remaining);
} // end of remaining()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets the request state to the given value.
* @param state the latest request state.
*/
/* package */ void state(final RequestState state)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("Reply %d.%d: setting state to %s.",
_replier.feedId(),
mFeedId,
state));
}
_requestState = state;
// If the new state means that the request is
// completed (whether successfully or not), remove
// this request from the reply feed and close this
// feed.
if (state == RequestState.DONE ||
state == RequestState.CANCELED)
{
_replier.requestDone(this);
close();
}
return;
} // end of state(RequestState)
/**
* Sets the number of remaining remote repliers for this
* request.
* @param n remote replier count.
*/
/* package */ void remoteRemaining(final int n)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("Reply %d.%d: number of %s repliers remaining is %,d.",
_replier.feedId(),
mFeedId,
_replier.location(),
n));
}
// Have the request feed update its remaining count.
_requestor.updateRemaining(_remaining, n);
_remaining = n;
return;
} // end of remoteRemaining(int)
//
// end of Set Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Local Client API.
// Applications should use the following methods to send
// a request or a reply to a request.
//
/**
* Forwards the reply message to the request feed. If
* this is a final reply (whether OK or error), the
* request state is set to done.
* @param msg the reply message.
* @throws IllegalArgumentException
* if {@code msg} is {@code null} or is not a valid reply
* message class for this request.
* @throws IllegalStateException
* if the request state is {@link RequestState#DONE done}.
*/
public void reply(final EReplyMessage msg)
throws IllegalArgumentException,
IllegalStateException
{
// Is there a message to send?
if (msg == null)
{
throw (
new IllegalArgumentException("msg is null"));
}
// Is this reply message valid for the given request?
if (!(_replier.isValidReply(msg.key())))
{
// No, wrong message type.
throw (
new IllegalArgumentException(
((msg.key()).messageClass()).getName() +
" is not a valid reply message class for " +
((_request.key()).messageClass()).getName()));
}
if (!(((msg.key()).subject()).equals(
(mSubject.key()).subject())))
{
throw (
new IllegalArgumentException(
"reply subject \"" +
(msg.key()).subject() +
"\" does not match request message subject \"" +
(mSubject.key()).subject() +
"\""));
}
// Is this request still in-progress?
if (_requestState == RequestState.DONE)
{
throw (
new IllegalStateException(
"request is done"));
}
// Everything checks out. Is this request finished?
if (msg.isFinal() && (_remaining - 1) == 0)
{
// Yes. Mark that done.
--_remaining;
state(RequestState.DONE);
}
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("Reply %d.%d: forwarding reply message:%n%s",
_replier.feedId(),
mFeedId,
msg));
}
else if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("Reply %d.%d: forwarding reply message %s.",
_replier.feedId(),
mFeedId,
msg.key()));
}
// Now hand this reply off to the originating request
// feed and that feed will forward it to the client.
_requestor.reply(_remaining, msg, this);
return;
} // end of reply(EReplyMessage)
/**
* Forwards a remote reply message to the request feed.
* If there are no more remote replies, the request state
* is set to done.
* @param msg the reply message.
*/
/* package */ void remoteReply(final EReplyMessage msg)
{
// Did the remote eBus previously report no more
// replies?
if (_remaining == 0)
{
state(RequestState.DONE);
}
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("Reply %d.%d: forwarding reply message:%n%s",
_replier.feedId(),
mFeedId,
msg));
}
else if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("Reply %d.%d: forwarding %s.",
_replier.feedId(),
mFeedId,
msg.key()));
}
// Now hand this reply off to the originating request
// feed and that feed will forward it to the client.
_requestor.reply(_remaining, msg, this);
return;
} // end of remoteReply(EReplyMessage)
//
// end of Local Client API.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
/**
* Post reply messages and cancel replies to this request
* feed. The requestor tracks the request state.
*/
private final ERequestFeed.ERequest _requestor;
/**
* Mark replies as coming from this feed. The request
* feed needs to know from where replies originate.
*/
private final EReplyFeed _replier;
/**
* The request message itself.
*/
private final ERequestMessage _request;
/**
* Contains the functional interface callback for request
* messages. If not explicitly set by client, then
* defaults to
* {@link EReplier#cancelRequest(EReplyFeed.ERequest)}.
*/
private final CancelRequestCallback _cancelCallback;
/**
* Tracks whether this request for its replier is active,
* cancel in-progress, or done. Initialize to active.
*/
private RequestState _requestState;
/**
* Tracks the number of remaining repliers for this request.
* For a local replier, this will always be one. For a remote
* replier, this may be > one.
*/
private int _remaining;
} // end of class ERequest
/**
* This task forwards an eBus request to
* {@link EReplier#request(EReplyFeed.ERequest)}.
*/
private static final class RequestTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Pass the request to the application via this callback.
*/
private final RequestCallback _callback;
//---------------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new request task for the given message and
* feed.
* @param request the eBus request instance. Post replies
* to {@link ERequest#reply(EReplyMessage)}.
* @param cb method callback.
*/
private RequestTask(final EReplyFeed.ERequest request,
final RequestCallback cb)
{
super (request);
_callback = cb;
} // end of RequestTask(ERequest, RequestCallback)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementations.
//
/**
* Passes the arguments to
* {@link EReplier#request(EReplyFeed.ERequest)}, logging
* any client-thrown exception.
*/
@Override
public void run()
{
final Object target = (mFeed.eClient()).target();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(this.toString());
}
if (target != null)
{
try
{
_callback.call((EReplyFeed.ERequest) mFeed);
}
catch (Throwable tex)
{
final String reason =
String.format(
"RequestTask[%s, %s] exception",
(target.getClass()).getName(),
mFeed);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(Level.WARNING, reason, tex);
}
else
{
sLogger.log(Level.WARNING, reason);
}
}
}
return;
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (String.format("RequestTask[%s]", mFeed));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of class RequestTask
/**
* This task forwards a cancel request to
* {@link EReplier#cancelRequest(net.sf.eBus.client.EReplyFeed.ERequest)}.
*/
private static final class CancelRequestTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Canceling this request.
*/
private final EReplyFeed.ERequest mRequest;
/**
* Pass the request cancellation via this callback.
*/
private final CancelRequestCallback mCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new CancelRequestTask instance for the given
* request identifier.
* @param request canceling this request.
* @param feed the canceled request applies to this feed.
* @param cb the method callback.
*/
private CancelRequestTask(final EReplyFeed.ERequest request,
final EReplyFeed feed,
final CancelRequestCallback cb)
{
super (feed);
mRequest = request;
mCallback = cb;
} // end of CancelRequestTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementations.
//
/**
* Passes the arguments to the
* {@link EReplier#cancelRequest(EReplyFeed.ERequest)},
* logging any client-thrown exception.
*/
@Override
public void run()
{
final Object target = (mFeed.eClient()).target();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(this.toString());
}
if (target != null)
{
try
{
mCallback.call(mRequest);
}
catch (Throwable tex)
{
final String reason =
String.format(
"EReplier.cancelRequest(%s) exception",
mFeed);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(Level.WARNING, reason, tex);
}
else
{
sLogger.log(Level.WARNING, reason);
}
}
}
return;
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return ("CancelRequestTask");
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of class CancelRequestTask
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link EReplier#request(EReplyFeed.ERequest)} method name.
*/
/* package */ static final String REQUEST_METHOD = "request";
/**
* {@link EReplier#cancelRequest(EReplyFeed.ERequest)} method
* name.
*/
/* package */ static final String CANCEL_METHOD =
"cancelRequest";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
Logger.getLogger(EReplyFeed.class.getName());
//-----------------------------------------------------------
// Locals.
//
/**
* Use this condition to check if the request message should
* be forwarded to subscriber.
*/
private final ECondition mCondition;
/**
* The active requests accepted by this reply feed.
*/
private final List mRequests;
/**
* The request message data type. Used to determine if reply
* messages belong to the
* {@link net.sf.eBus.messages.EReplyInfo} allowed message
* types.
*/
private final MessageType mDataType;
/**
* Contains the functional interface callback for request
* messages. If not explicitly set by client, then defaults
* to
* {@link EReplier#request(EReplyFeed.ERequest)}.
*/
private RequestCallback mRequestCallback;
/**
* Contains the functional interface callback for request
* messages. If not explicitly set by client, then defaults
* to
* {@link EReplier#cancelRequest(EReplyFeed.ERequest)}.
*/
private CancelRequestCallback mCancelCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new reply feed for the given client and request
* subject.
* @param client the application client.
* @param scope whether this feed support local feeds, remote
* feeds, or both.
* @param condition the replier accepts requests only if it
* passes this condition.
* @param dataType the request message type. Used to
* determine if the reply message is in the request message
* {@link net.sf.eBus.messages.EReplyInfo} list.
* @param subject this feed is associated with this request
* subject.
*/
private EReplyFeed(final EClient client,
final FeedScope scope,
final ECondition condition,
final MessageType dataType,
final ERequestSubject subject)
{
super (client, scope, FeedType.REPLY_FEED, subject);
mCondition = condition;
mRequests = new ArrayList<>();
mDataType = dataType;
mRequestCallback = null;
mCancelCallback = null;
} // end of EReplyFeed(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EFeed Interface Implementation.
//
/**
* If the advertisement is in place, then un-advertises it,
* sends a final error reply to all extant requests, and if
* this feed's scope is local & remote or remote only,
* removes this feed from the advertisers list.
*
* @see EFeed#close()
*/
@Override
protected void inactivate()
{
if (mInPlace)
{
((ERequestSubject) mSubject).unadvertise(this);
mInPlace = false;
// Fail all extant requests since this replier is no
// longer in business.
failRequests("replier closed");
}
// Remove from the advertisers list if the client is
// local and the feed supports remote clients.
if (mEClient.isLocal() &&
(mScope == FeedScope.LOCAL_AND_REMOTE ||
mScope == FeedScope.REMOTE_ONLY))
{
mAdvertisers.remove(this);
}
return;
} // end of inactivate()
/**
* Always returns zero because reply feeds do no have the
* concept of activation.
* @param loc contra-feed location.
* @param fs contra-feed state.
* @return zero.
*/
@Override
/* package */ int updateActivation(final ClientLocation loc,
final EFeedState fs)
{
return (0);
} // end of updateActivation(ClientLocation, EFeedState)
//
// end of EFeed Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if this reply feed is both open and
* advertised; otherwise, returns {@code false}.
* @return {@code true} if this reply feed is open and
* advertised.
*/
@Override
public boolean isAdvertised()
{
return (mIsActive.get() && mInPlace);
} // end of isAdvertised()
/**
* Returns {@code true} if {@code repKey} is an allowed
* reply message for this request and {@code false} if
* not. A reply which is not allowed for the request will not
* be sent to the requester.
* @param repKey the reply message key.
* @return {@code true} if the reply message key is allowed
* for this request.
*/
/* package */ boolean isValidReply(final EMessageKey repKey)
{
return (mDataType.isValidReply(repKey.messageClass()));
} // end of isValidReply(EMessageKey)
//
// end of Get Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* Puts the new request callback in place. If {@code cb} is
* not {@code null}, requests will be passed to {@code cb}
* rather than
* {@link EReplier#request(EReplyFeed.ERequest)}. A
* {@code null cb} means that requests are passed to the
* {@link EReplier#request(EReplyFeed.ERequest)} override.
* @param cb the request callback. May be {@code null}.
* @throws IllegalStateException
* if this feed is either closed or advertised.
*/
@Override
public void requestCallback(final RequestCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"advertisement in place"));
}
mRequestCallback = cb;
return;
} // end of requestCallback(RequestCallback)
/**
* Puts the cancel request callback in place. If {@code cb}
* is not {@code null}, requests will be passed to {@code cb}
* rather than
* {@link EReplier#cancelRequest(EReplyFeed.ERequest)}. A
* {@code null cb} means that cancellations are passed to the
* {@link EReplier#cancelRequest(EReplyFeed.ERequest)}
* override.
* @param cb the cancel request callback. May be
* {@code null}.
* @throws IllegalStateException
* if this feed is either closed or advertised.
*/
@Override
public void cancelRequestCallback(final CancelRequestCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"advertisement in place"));
}
mCancelCallback = cb;
return;
} // end of cancelRequestCallback(CancelRequestCallback)
/**
* Updates the replier feed state to the given value. If
* {@code update} equals the currently stored reply feed
* state, nothing is done. Otherwise, the updated value is
* stored. If this feed is advertised to the server and
* the subscription feed is up, then this update is forwarded
* to subscribed requestors.
* @param update the new reply feed state.
* @throws NullPointerException
* if {@code update} is {@code null}.
* @throws IllegalStateException
* if this feed was closed and is inactive or is not
* advertised.
*/
@Override
public void updateFeedState(final EFeedState update)
{
Objects.requireNonNull(update, "update is null");
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (!mInPlace)
{
throw (
new IllegalStateException(
"feed not advertised"));
}
// Does this update actually change anything?
if (update != mFeedState)
{
// Yes. Apply the update.
mFeedState = update;
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s replier %d, feed %d: setting %s feed state to %s (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
update,
mScope));
}
// Forward the update to the subject.
((ERequestSubject) mSubject).updateFeedState(this);
}
// No change. Nothing to do.
return;
} // end of updateFeedState(EFeedState)
/**
* Returns {@code true} if the replier is still active and
* the request message passes the reply condition. If this
* method returns {@code true}, then
* {@link #request(ERequestFeed.ERequest, ERequestMessage)}
* is used to pass the request to the replier.
* @param msg test this request message against the reply
* condition.
* @param loc requestor location.
* @return {@code true} if {@code msg} is acceptable.
*
* @see #open(EReplier, EMessageKey, EFeed.FeedScope, ECondition)
* @see #request(ERequestFeed.ERequest, ERequestMessage)
*/
/* package */ boolean checkRequest(final ERequestMessage msg,
final ClientLocation loc)
{
return (mIsActive.get() &&
mCondition.test(msg) &&
mScope.supports(loc));
} // end of checkRequest(ERequestMessage, ClientLocation)
/**
* Forwards the given request message and request object to
* the client, returning a {@link ERequest} object. Note that
* {@link #checkRequest(ERequestMessage, EClient.ClientLocation)}
* was previously called to determine if the replier accepts
* the request message.
* {@link ERequestSubject} can determine if any repliers
* accepted the request. If no repliers accepted or
* this reply is inactive, then returns {@code null}.
* @param request the request associated with the
* message.
* @param message the request message to be posted to replier
* clients.
* @return request instance linked to
* {@link ERequestFeed.ERequest}. May be {@code null}.
*
* @see #checkRequest(ERequestMessage, EClient.ClientLocation)
*/
/* package */ EReplyFeed.ERequest
request(final ERequestFeed.ERequest request,
final ERequestMessage message)
{
EReplyFeed.ERequest retval;
// Is this feed still alive?
// Is this feed up?
if (!mIsActive.get() ||
mFeedState == EFeedState.DOWN)
{
// No, this feed is gone or down.
retval = null;
}
else
{
// Yes. Add the request to the active requests list
// BUT DO NOT DISPATCH THE REQUEST TO THE REPLIER
// CLIENT.
// The bookkeeping needed to process the reply is
// not yet in place.
retval = new EReplyFeed.ERequest(request,
this,
message,
mCancelCallback);
synchronized (mRequests)
{
mRequests.add(retval);
}
}
return (retval);
} // end of request(ERequest, ERequestMessage)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Returns a reply advertiser feed for the specified request
* message class and subject. The calls {@link #advertise}
* on the returned feed in order to start receiving the
* specified request messages.
* @param client the application object subscribing to the
* request. May not be {@code null}.
* @param key the request message class and subject.
* May not be {@code null} and must reference a request
* message class.
* @param scope whether this feed supports only local
* clients, remote clients, or both.
* @param condition accept request messages only if the
* messages passes this condition. May be {@code null}. If
* {@code null}, then the
* {@link #NO_CONDITION default condition} which accepts all
* request messages is used.
* @return the newly created reply feed.
* @throws NullPointerException
* if any of the required parameters is {@code null}.
* @throws IllegalArgumentException
* if any of the given parameters is invalid.
*
* @see #advertise()
* @see EFeed#close()
*/
@SuppressWarnings ("unchecked")
public static EReplyFeed open(final EReplier client,
final EMessageKey key,
final FeedScope scope,
ECondition condition)
{
// Validate the parameters.
// Are the parameters non-null references?
Objects.requireNonNull(client, "client is null");
Objects.requireNonNull(key, "key is null");
Objects.requireNonNull(scope, "scope is null");
// Is the message key for a request?
if (!key.isRequest())
{
throw (
new IllegalArgumentException(
String.format(
"%s is not a request message",
key)));
}
final MessageType dataType =
(MessageType) DataType.findType(key.messageClass());
return (open(client,
key,
scope,
condition,
ClientLocation.LOCAL,
dataType,
false));
} // end of open(...)
/**
* Returns a reply advertiser feed for the specified request
* message class and subject. The calls {@link #advertise}
* on the returned feed in order to start receiving the
* specified request messages.
*
* This method does not parameter validation since this is
* a {@code package private} method.
*
* @param client the application object subscribing to the
* request. May not be {@code null}.
* @param key the request message class and subject.
* May not be {@code null} and must reference a request
* message class.
* @param scope whether this feed supports only local
* clients, remote clients, or both.
* @param condition accept request messages only if the
* messages passes this condition. May be {@code null}. If
* {@code null}, then the
* {@link #NO_CONDITION default condition} which accepts all
* request messages is used.
* @param l {@code client} location.
* @param dataType the request message type. Used to
* determine if the reply message is in the request message
* {@link net.sf.eBus.messages.EReplyInfo} list.
* @param isMulti {@code true} if this is part of a multiple
* key feed. If {@code true}, this feed is not added to the
* client feed list.
* @return the newly created reply feed.
*/
public static EReplyFeed open(final EReplier client,
final EMessageKey key,
final FeedScope scope,
final ECondition condition,
final ClientLocation l,
final MessageType dataType,
final boolean isMulti)
{
// If a null condition was passed in, then use the
// default NO_CONDITION.
final ECondition replyCondition =
(condition == null ? NO_CONDITION : condition);
final EClient eClient =
EClient.findOrCreateClient(client, l);
final ERequestSubject subject =
ERequestSubject.findOrCreate(key);
final EReplyFeed retval =
new EReplyFeed(eClient,
scope,
replyCondition,
dataType,
subject);
// Let the client know it is being referenced by another
// feed - but only if this is not part of a multiple key
// feed. In that case, the multiple key feed is added to
// the client.
if (!isMulti)
{
eClient.addFeed(retval);
}
// Add to the advertiser list if the client is local and
// the feed supports remote clients.
if (eClient.isLocal() &&
(scope == FeedScope.LOCAL_AND_REMOTE ||
scope == FeedScope.REMOTE_ONLY))
{
mAdvertisers.add(retval);
}
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s replier %d, Feed %d: opened %s (%s).",
l,
eClient.clientId(),
retval.feedId(),
key,
scope));
}
return (retval);
} // end of open(...)
/**
* Advertises this replier fed to the associated request
* subject. If this feed is currently advertised to the
* subject, then does nothing.
* @throws IllegalStateException
* if this feed is closed or if the client did not override
* {@link EReplier} methods nor put the required callbacks in
* place.
*
* @see #unadvertise()
* @see #updateFeedState(EFeedState)
* @see EFeed#close()
*/
@Override
public void advertise()
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (!mInPlace)
{
// If the callbacks were not put in place, then use
// the defaults.
if (mRequestCallback == null)
{
// Did the client override the request method?
if (!isOverridden(REQUEST_METHOD,
EReplyFeed.ERequest.class))
{
// That's a Bozo no-no.
throw (
new IllegalStateException(
REQUEST_METHOD +
" not overridden and requestCallback not set"));
}
mRequestCallback =
((EReplier) mEClient.target())::request;
}
if (mCancelCallback == null)
{
// Did the client override the cancelRequest
// method?
if (!isOverridden(CANCEL_METHOD,
EReplyFeed.ERequest.class))
{
// That's a negative.
throw (
new IllegalStateException(
CANCEL_METHOD +
" not overridden and cancelRequestCallback not set"));
}
mCancelCallback =
((EReplier) mEClient.target())::cancelRequest;
}
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s replier %d, Feed %d: advertising %s (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope));
}
((ERequestSubject) mSubject).advertise(this);
mInPlace = true;
}
return;
} // end of subscribe()
/**
* Retracts this replier feed from the associated request
* subject. Does nothing if this feed is not currently
* advertised or closed.
* @throws IllegalStateException
* if this feed was closed and is inactive.
*
* @see #advertise()
* @see EFeed#close()
*/
@Override
public void unadvertise()
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s replier %d, Feed %d: unadvertising (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mScope));
}
((ERequestSubject) mSubject).unadvertise(this);
mInPlace = false;
mActivationCount = 0;
mFeedState = EFeedState.DOWN;
// Fail all extant requests since this replier is no
// longer in business.
failRequests("replier unadvertised");
}
return;
} // end of unsubscribe()
/**
* Dispatch {@code request} to the replier client.
* @param request post this request to the replier.
*/
/* package */ void dispatch(final EReplyFeed.ERequest request)
{
mEClient.dispatch(
new RequestTask(request, mRequestCallback));
return;
} // end of dispatch(EReplyFeed.ERequest)
/**
* Removes the completed request from the requests list.
* @param request the newly completed request.
*/
/* package */ void requestDone(final ERequest request)
{
synchronized (mRequests)
{
mRequests.remove(request);
}
return;
} // end of requestDone(ERequest)
/**
* Posts an error reply containing the given reason to each
* outstanding requests and then clears the request list.
* @param reason reason why the request is being rejected.
*/
private void failRequests(final String reason)
{
final List requests = new ArrayList<>();
// Clear out the now defunct requests and work off of the
// copy.
synchronized (mRequests)
{
requests.addAll(requests);
mRequests.clear();
}
requests.forEach(
request ->
{
request.reply(
new EReplyMessage(
(request.key()).subject(),
ReplyStatus.ERROR,
reason));
});
return;
} // end of failRequests(String)
} // end of class EReplyFeed