
net.sf.eBus.client.ERequestFeed 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.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.messages.EMessage;
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 ERequestFeed} is the application entry point for
* posting {@link ERequestMessage request messages} to repliers.
* Follow these steps to use this feed:
*
* Step 1: Implement the {@link ERequestor}
* interface.
*
*
* Step 2:
* {@link #open(ERequestor, EMessageKey, EFeed.FeedScope) Open}
* a request feed for a given {@code ERequestor} instance and
* {@link EMessageKey type+topic message key}.
*
*
* Step 3 (optional): Do not override
* {@link ERequestor} interface methods. Instead, set callbacks
* using {@link #statusCallback(FeedStatusCallback)} and/or
* {@link #replyCallback(ReplyCallback)} passing in Java lambda
* expressions.
*
*
* Step 4:
* {@link #subscribe() Subscribe} to reply message key.
*
*
* Step 5:
* {@link #request(ERequestMessage) Send} a request to one or
* more advertised repliers. If there are no advertised repliers,
* then {@code request(ERequestMessage)} returns
* {@link RequestState#DONE}, signaling that the requestor will
* not receive any replies.
*
*
* Step 6: Wait for
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest) replies}
* to arrive. When the {@code remaining} parameter is zero, that
* means this is the final reply from all repliers. When
* {@link EReplyMessage#replyStatus} is
* {@link ReplyStatus#ERROR} or {@link ReplyStatus#OK_FINAL},
* then this is the final reply from that replier.
*
*
* Step 7: A requestor may terminate a request
* prior to completion by calling
* {@link ERequestFeed.ERequest#close()}. No more replies should
* be received once this is done. (Note: there is a possibility
* that an in-flight reply will still be posted to the
* requestor.)
*
*
* Step 8: When
* requestor is shutting down, {@link #unsubscribe() retract} the
* subscription and {@link #close close} the feed.
*
* Example use of {@code ERequestFeed}
* import java.util.ArrayList;
import java.util.List;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.ERequestFeed;
import net.sf.eBus.client.ERequestor;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
Step 1: Implement the ERequestor interface.
public class CatalogRequestor implements ERequestor {
public CatalogRequestor(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 request feed.
mFeed = ERequestFeed.open(this, mKey, mScope);
Step 3: ERequestor interface methods overridden.
Step 4: Subscribe to reply message key.
mFeed.subscribe();
} catch (IllegalArgumentException argex) {
// Open failed. Place recovery code here.
}
}
@Override
public void shutdown() {
synchronized (mRequests) {
for (ERequestFeed.ERequest request : mRequests) {
Step 7: Cancel request by closing request.
request.close();
}
mRequests.clear();
}
Step 8: On shutdown, close request feed.
if (mFeed != null) {
mFeed.close();
mFeed = null;
}
}
@Override
public void feedStatus(final EFeedState feedState, final ERequestFeed feed) {
if (feedState == EFeedState.DOWN) {
// Down. There are no repliers.
} else {
// Up. There is at least one replier.
}
}
Step 6: Wait for replies to request.
@Override
public void reply(final int remaining,
final EReplyMessage reply,
final ERequestFeed.ERequest request) {
final String reason = msg.replyReason();
if (msg.replyStatus == EReplyMessage.ReplyStatus.ERROR)
{
// The replier rejected the request. Report the reason
} else if (msg.replyStatus == EReplyMessage.ReplyStatus.OK_CONTINUING) {
// The replier will be sending more replies.
} else {
// This is the replier's last reply.
}
if (remaining == 0) {
synchronized (mRequests) {
mRequests.remove(request);
}
}
}
Step 5: Send a request message.
public void placeOrder(final String product,
final int quantity,
final Price price,
final ShippingEnum shipping,
final ShippingAddress address) {
final CatalogOrder msg =
new CatalogOrder(product, quantity, price, shipping, address);
try {
synchronized (mRequests) {
mRequests.add(mFeed.request(msg));
}
} catch (Exception jex) {
// Request failed. Put recovery code here.
}
}
private final EMessageKey mKey;
private final FeedScope mScope;
private final List<ERequestFeed.ERequest> mRequests;
private ERequestFeed mFeed;
}
*
* @see ERequestor
* @see EReplier
* @see EReplyFeed
*
* @author Charles Rapp
*/
public final class ERequestFeed
extends ESingleFeed
implements IERequestFeed
{
//---------------------------------------------------------------
// Inner classes.
//
/**
* This class represents an individual request, tracking the
* current request state and the remaining repliers. This
* class acts as a conduit from the {@link EReplier} to the
* {@link ERequestor}, routing replies back to the requestor.
*
* A requestor can cancel an in-progress request by calling
* {@link #close()}. Once canceled, no new replies
* will be delivered to the requestor. However, in-flight
* replies already scheduled for delivery will be delivered.
*
*
* Note: Applications are responsible for
* tracking active requests once instantiated.
* {@link ERequestFeed} does not track active requests for
* the application.
*
*/
public static final class ERequest
extends ESingleFeed
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* The repliers actively responding to this request. When
* a replier completes its response, the replier is
* removed from this list. When the list is empty, the
* request state is {@link RequestState#DONE}.
*
* Maps the replier's client identifier to its request
* instance.
*
*/
private final List mRepliers;
/**
* Pass replies to application via this callback.
*/
private final Map, ReplyCallback> mReplyCallbacks;
/**
* Tracks the number of remaining repliers. This tally
* may be > {@code _repliers.size()} because a single
* remote {@code EReplyFeed.ERequest} stands in for
* multiple repliers.
*/
private int mRemaining;
/**
* The request current state. Initialized to
* {@link RequestState#NOT_PLACED}.
*/
private RequestState mRequestState;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new request for the given replier
* @param feed request feed placing this request.
* @param cbs reply message key to reply callback method
* map.
*/
private ERequest(final ERequestFeed feed,
final Map, ReplyCallback> cbs)
{
super (feed.mEClient,
feed.mScope,
feed.mFeedType,
feed.mSubject);
mRepliers = new ArrayList<>();
mRemaining = 0;
mRequestState = RequestState.NOT_PLACED;
mReplyCallbacks = cbs;
} // end of ERequest(ERequestFeed, Map<>)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// EFeed Abstract Method Overrides.
//
/**
* If this request is still active, then automatically
* cancels this request with all extant repliers. No
* further replies will be delivered to the requestor
* unless previously scheduled for delivery.
*/
@Override
protected synchronized void inactivate()
{
// Is the request active or cancel-in-progress?
if (mRequestState == RequestState.ACTIVE)
{
// Yes. Tell the remaining repliers that this
// request is finished.
mRepliers.stream()
.forEach((replier) ->
{
replier.close();
});
// Clear out the repliers list and mark this
// request as canceled.
mRepliers.clear();
setState(RequestState.CANCELED);
}
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 (String.format("%s request %d",
mEClient.location(),
mFeedId));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns the request current state.
* @return request current state.
*/
public RequestState requestState()
{
return (mRequestState);
} // end of requestState()
/**
* Returns the number of repliers still replying to the
* request. Will be zero if the request state is
* {@link RequestState#NOT_PLACED} or
* {@link RequestState#DONE}.
* @return number of in-progress repliers.
*/
public int repliersRemaining()
{
return (mRemaining);
} // end of repliersRemaining()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets the number of repliers still processing the
* request.
* @param count number of remaining repliers. May
* be > {@code repliers.size()}.
* @param repliers repliers.
*/
/* package */ void
repliers(final int count,
final Collection repliers)
{
mRepliers.addAll(repliers);
mRemaining = count;
// The request is now active.
setState(RequestState.ACTIVE);
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: %d remaining (active=%b, state=%s)",
this,
mRemaining,
mIsActive.get(),
mRequestState));
}
return;
} // end of repliers(int, Collection<>)
//
// end of Set Methods.
//-------------------------------------------------------
/**
* Forwards the reply message to the request client. If
* this is a final reply from {@code replier}, then the
* replier is removed from the repliers map.
* @param remaining number of repliers remaining. Will be
* zero for a local replier.
* @param msg a reply message to this request.
* @param replier the reply is from this reply feed.
*/
/* package */ void reply(final int remaining,
final EReplyMessage msg,
final EReplyFeed.ERequest replier)
{
final boolean isActive = mIsActive.get();
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: %s reply, %s final, %d remaining (active=%b, state=%s)",
this,
replier.location(),
(msg.isFinal() ? "is" : "is not"),
remaining,
isActive,
mRequestState));
}
// Is this feed still alive?
// Is this request still active?
if (isActive &&
mRequestState == RequestState.ACTIVE)
{
// Is this the replier's final reply?
if (msg.isFinal())
{
// Yes. Remove the replier from the list.
mRepliers.remove(replier);
// Is this a *local* replier?
if (replier.location() ==
ClientLocation.LOCAL)
{
// Yes. Remove it from the replier count.
// Note: remote repliers update their count
// by calling updateRemaining().
--mRemaining;
}
// Is this the last of the repliers?
if (mRemaining == 0)
{
// Then this request is finished BUT DO
// NOT CLOSE THIS FEED.
//
// Why?
// Because the requestor may be using the
// request's feed identifier to track the
// request. Once closed, the feed
// identifier will be recycled.
//
// Why is this a problem? The request
// feed identifier is immutable.
//
// Consider the following scenario. This
// now defunct request is feed ID 1. We
// close the request and toss ID 1 back
// into the ID pool for re-use. Then we
// asynchronously post this reply to the
// requestor. While the reply is en
// route, the requestor creates a new
// request AND IT IS ASSIGNED FEED ID 1.
// Now when the reply for the first
// feed ID 1 arrives, the requestor will
// think it is for the second feed ID 1.
//
// Solution:
// Have ReplyTask.run() check the request
// start *after* delivering the reply.
// If the request is done or canceled,
// then close the request and recycle the
// feed identifier. If the requestor
// hangs on to a feed identifier after
// receiving the final reply, then that
// is an application bug.
setState(RequestState.DONE);
}
}
// Foward the response to the requesting client.
// Note: since replies come from at various times,
// the caller has *not* acquired the dispatch lock
// for this method.
mEClient.dispatch(
new ReplyTask(
mRemaining,
msg,
this,
mReplyCallbacks.get((msg.key()).messageClass())));
}
return;
} // end of reply(int, EReplyMessage, ERequest)
/**
* Update the remaining replier count.
* @param previous previous remaining replier count.
* @param next next remaining replier count.
*/
/* package */ void updateRemaining(final int previous,
final int next)
{
mRemaining += (next - previous);
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: %d remaining (active=%b, state=%s)",
this,
mRemaining,
mIsActive.get(),
mRequestState));
}
return;
} // end of updateRemaining(int, int)
/**
* Sets the next request state, logging the state change.
* If the state is either {@link RequestState#DONE} or
* {@link RequestState#CANCELED}, then the request
* @param nextState next request state.
*/
private void setState(final RequestState nextState)
{
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: %s -> %s.",
this,
mRequestState,
nextState));
}
mRequestState = nextState;
return;
} // end of setState(RequestState)
} // end of class ERequest
/**
* This task calls back
* {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}.
*/
private static final class FeedStatusTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* The replier new feed status.
*/
private final EFeedState mFeedState;
/**
* Pass the feed state to this method.
*/
private final FeedStatusCallback mCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new feed status task for the given callback
* parameters.
* @param feedState {@code true} if the feed is up and
* {@code false} if down.
* @param feed the feed state applies to this request
* feed.
* @param cb callback method.
*/
private FeedStatusTask(final EFeedState feedState,
final ERequestFeed feed,
final FeedStatusCallback cb)
{
super (feed);
mFeedState = feedState;
mCallback = cb;
} // end of FeedStatusTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementations.
//
/**
* Issues the
* {@link ESubscriber#feedStatus(EFeedState, ESubscribeFeed)}
* callback, logging any client-thrown exception.
*/
@Override
@SuppressWarnings ("unchecked")
public void run()
{
final Object target = (mFeed.eClient()).target();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(this.toString());
}
if (target != null)
{
try
{
mCallback.call(
mFeedState, (ERequestFeed) mFeed);
}
catch (Throwable tex)
{
final String reason =
String.format(
"%s exception",
(target.getClass()).getName());
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("FeedStatusTask [feed=%s, state=%s]",
mFeed,
mFeedState));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of FeedStatusTask
/**
* This task is used to forward an eBus reply message to
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}.
*/
private static final class ReplyTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* The number of outstanding repliers.
*/
private final int mRemaining;
/**
* Deliver this message to the requestor.
*/
private final EReplyMessage mMessage;
/**
* The reply is for this request.
*/
private final ERequestFeed.ERequest mRequest;
/**
* Pass the reply message back to the application via
* this callback.
*/
private final ReplyCallback mCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new reply task for the given reply message
* and requestor.
* @param remaining number of repliers yet to finish
* replying.
* @param msg the reply message.
* @param request the reply is for this request.
* @param cb reply callback method.
*/
private ReplyTask(final int remaining,
final EReplyMessage msg,
final ERequestFeed.ERequest request,
final ReplyCallback cb)
{
super (request);
mRemaining = remaining;
mMessage = msg;
mRequest = request;
mCallback = cb;
} // end of ReplyTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementations.
//
/**
* Passes the arguments to the client's reply method.
* Catches any client-thrown exception and logs it.
*/
@Override
@SuppressWarnings ("unchecked")
public void run()
{
final Object target = (mFeed.eClient()).target();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(this.toString());
}
if (target != null)
{
try
{
mCallback.call(
mRemaining, mMessage, mRequest);
}
catch (Throwable tex)
{
final String reason =
String.format(
"ReplyTask[%s, ] exception",
(target.getClass()).getName(),
mMessage.key());
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(Level.WARNING, reason, tex);
}
else
{
sLogger.log(Level.WARNING, reason);
}
}
}
// Is the request dead?
if (mRequest.requestState() == RequestState.DONE ||
mRequest.requestState() == RequestState.CANCELED)
{
// Yes. NOW it is safe to close the feed now that
// the reply is safely delivered.
mRequest.close();
}
return;
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (
String.format(
"ReplyTask[remaining=%d, key=%s]",
mRemaining,
mMessage.key()));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of class ReplyTask
//---------------------------------------------------------------
// Enums.
//
/**
* A request is either not placed, active, done, or canceled.
*/
public enum RequestState
{
/**
* Request instantiated but not yet placed.
*/
NOT_PLACED,
/**
* Request message sent to repliers and is waiting for
* reply messages. When the final reply message is
* received, the request goes to the {@link #DONE} state.
* Goes to the {@link #CANCELED} state if the
* requestor calls
* {@link ERequestFeed.ERequest#close()}.
*/
ACTIVE,
/**
* The request is no longer active because all replies
* are received. The request feed may not be
* used to send a new request message.
*/
DONE,
/**
* The request was canceled by client and will no
* longer respond to replies. The request feed may
* not be used to send a new request message.
*/
CANCELED
} // end of RequestState
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}
* method name.
*/
public static final String FEED_STATUS_METHOD = "feedStatus";
/**
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}
* method name.
*/
public static final String REPLY_METHOD = "reply";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
Logger.getLogger(ERequestFeed.class.getName());
//-----------------------------------------------------------
// Locals.
//
/**
* The repliers actively responding to this request feed.
* Repliers are added when new repliers subscribe and
* removed when existing repliers un-subscribe.
*/
private final List mRepliers;
/**
* Contains the functional interface callback for feed
* status updates. If not explicitly set by client, then
* defaults to
* {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}.
*/
private FeedStatusCallback mStatusCallback;
/**
* Maps the reply message key to the functional interface
* callback for that reply. If a reply message is not
* explicitly set by the client, then defaults to
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}.
*/
private final Map, ReplyCallback> mReplyCallbacks;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new request feed for the given client,
* request subject, and scope.
* @param client the application client.
* @param scope whether this request matches replies local
* only to this JVM, remote replies, or both.
* @param subject the request feed is associated with this
* request subject.
* @param cbs reply callback map with all message keys
* set to {@code null} callbacks.
*/
private ERequestFeed(final EClient client,
final FeedScope scope,
final ERequestSubject subject,
final Map, ReplyCallback> cbs)
{
super (client,
scope,
FeedType.REQUEST_FEED,
subject);
mRepliers = new ArrayList<>();
mStatusCallback = null;
mReplyCallbacks = cbs;
} // end of ERequestFeed(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EFeed Interface Implementation.
//
/**
* Cancels this feed's active requests
*/
@Override
protected synchronized void inactivate()
{
((ERequestSubject) mSubject).unsubscribe(this);
return;
} // end of inactivate()
@Override
/* package */ int updateActivation(final ClientLocation loc,
final EFeedState fs)
{
int retval = 0;
// Does this feed support the contra-feed's location?
if (mScope.supports(loc))
{
// Yes. Update the activation count based on the
// feed state.
boolean updateFlag = false;
// Increment or decrement?
if (fs == EFeedState.UP)
{
// Increment.
++mActivationCount;
retval = 1;
// Update?
updateFlag = (mActivationCount == 1);
}
// Decrement.
else if (mActivationCount > 0)
{
--mActivationCount;
retval = -1;
// Update?
updateFlag = (mActivationCount == 0);
}
// Did this feed transition between inactivation and
// activation?
if (updateFlag)
{
// Yes. Update the feed.
mFeedState = fs;
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s requestor %d, feed %d: setting %s feed state to %s (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
fs,
mScope));
}
mEClient.dispatch(
new FeedStatusTask(
fs, this, mStatusCallback));
}
}
// No, this location is not supported. Do not modify the
// activation count.
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s requestor %d, feed %d: %s feed state=%s, activation count=%d (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
fs,
mActivationCount,
mScope));
}
return (retval);
} // end of updateActivation(ClientLocation, EFeedState)
//
// end of EFeed Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* Puts the feed status callback in place. If {@code cb} is
* not {@code null}, feed status updates will be passed to
* {@code cb} rather than
* {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}.
* A {@code null cb} results in feed status updates posted to
* the
* {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}
* override.
* @param cb feed status update callback. May be
* {@code null}.
* @throws IllegalStateException
* if this feed is either closed or subscribed.
*/
@Override
public void statusCallback(final FeedStatusCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"subscription in place"));
}
mStatusCallback = cb;
return;
} // end of statusCallback(FeedStatusCallback<>)
/**
* Puts the reply message callback in place
* for all reply types. If {@code cb} is
* not {@code null}, replies will be passed to {@code cb}
* rather than
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}.
* A {@code null cb} results in replies posted to the
* {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)}
* override.
*
* Note that this method call overrides all previous calls
* to {@link #replyCallback(Class, ReplyCallback)}. If
* the goal is to use a generic callback for all replies
* except one specific message, then use this method to put
* the generic callback in place first and then use
* {@code replyCallback(EMessageKey, ReplyCallback)}.
*
* @param cb reply message callback. May be {@code null}.
* @throws IllegalStateException
* if this feed is either closed or subscribed.
*
* @see #replyCallback(Class, ReplyCallback)
*/
@Override
public void replyCallback(final ReplyCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"subscription in place"));
}
mReplyCallbacks.entrySet()
.forEach((entry) ->
{
entry.setValue(cb);
});
return;
} // end of replyCallback(ReplyCallback)
/**
* Sets the callback for a specific reply message class. If
* {@code cb} is not {@code null}, replies will be passed to
* {@code cb} rather than
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}.
* A {@code cb} results in replies posted to the
* {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)}
* override.
*
* If the goal is to set a single callback method for all
* reply message types, then use
* {@link #replyCallback(ReplyCallback)}. Note that method
* overrides all previous set reply callbacks.
*
* @param mc the reply message class.
* @param cb callback for the reply message.
* @throws NullPointerException
* if {@code mc} is {@code null}.
* @throws IllegalArgumentException
* if {@code mc} is not a reply for this request.
* @throws IllegalStateException
* if this feed is either closed or subscribed.
*/
@Override
public void replyCallback(final Class extends EReplyMessage> mc,
final ReplyCallback cb)
{
Objects.requireNonNull(mc, "mc is null");
if (!mReplyCallbacks.containsKey(mc))
{
throw (
new IllegalArgumentException(
mc.getSimpleName() +
" is not a " +
mSubject.key() +
" reply"));
}
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"subscription in place"));
}
mReplyCallbacks.put(mc, cb);
return;
} // end of replyCallback(Class, ReplyCallback)
/**
* Sets the reply callbacks to the given mappings. This
* method is used by {@link EMultiRequestFeed} to set
* the reply callback map in one call.
* @param cbs reply message callback map.
*/
/* package */ void replyCallbacks(Map, ReplyCallback> cbs)
{
mReplyCallbacks.putAll(cbs);
return;
} // end of replyCallbacks(Map<>)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Creates a new request feed for the given client, message
* key, and feed scope. The feed is assigned a unique
* identifier within the client's scope. This feed is used
* to {@link #request(ERequestMessage) create new requests}.
* @param client the eBus requestor opening this feed.
* @param key the request message key.
* @param scope whether this request matches local repliers,
* remote repliers, or both.
* @return the newly opened request feed.
* @throws NullPointerException
* if any of the required parameters is {@code null}.
* @throws IllegalArgumentException
* if {@code key} is not a request message.
*
* @see #request(ERequestMessage)
* @see EFeed#close()
*/
public static ERequestFeed open(final ERequestor client,
final EMessageKey key,
final FeedScope scope)
{
// 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)));
}
return (open(client,
key,
scope,
ClientLocation.LOCAL,
false));
} // end of open(...)
/**
* Subscribes this request feed to the eBus subject. There is
* no callback associated with request subscription. This
* subscription must be in place before
* {@link #request(ERequestMessage)} may be called.
* @throws IllegalStateException
* if this feed is closed or the client did not override
* {@link ERequestor} methods nor put the required callback
* in place.
*
* @see #unsubscribe()
* @see EFeed#close()
*/
@Override
public void subscribe()
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (!mInPlace)
{
final boolean replyOverride =
isOverridden(REPLY_METHOD,
int.class,
EReplyMessage.class,
ERequestFeed.ERequest.class);
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s requestor %d, feed %d: subscribing to %s (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope));
}
// If callbacks not explicitly set, then set them to
// the defaults.
if (mStatusCallback == null)
{
// Did the client override feedStatus?
if (!isOverridden(FEED_STATUS_METHOD,
EFeedState.class,
ERequestFeed.class))
{
// No? Gotta do one or the other.
throw (
new IllegalStateException(
FEED_STATUS_METHOD +
" not overridden and statusCallback not set"));
}
mStatusCallback =
((ERequestor) mEClient.target())::feedStatus;
}
mReplyCallbacks.entrySet()
.stream()
.filter(
(entry) ->
(entry.getValue() == null))
.forEachOrdered((entry) ->
{
// Did the client override reply?
if (!replyOverride)
{
// Nope. Not much point in
// putting this subscription
// in place.
throw (
new IllegalStateException(
REPLY_METHOD +
" not overridden and replyCallback not set"));
}
entry.setValue(
((ERequestor) mEClient.target())::reply);
});
((ERequestSubject) mSubject).subscribe(this);
mInPlace = true;
}
// Else nothing to do since the subscription is in place.
return;
} // end of subscribe()
/**
* Retracts this request feed from the associated subject.
* Does nothing if this feed is not currently subscribed.
* This feed may be re-subscribed after un-subscribing.
*
* Note that un-subscribing does not cancel any active
* requests or prevent delivery of replies to those requests.
*
*
* @see #subscribe()
* @see EFeed#close()
*/
@Override
public void unsubscribe()
{
// Is there a subscription to retract?
if (mInPlace)
{
// Yes. Retract the subscription.
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s requestor %d, feed %d: unsubscribing from %s (%s).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope));
}
((ERequestSubject) mSubject).unsubscribe(this);
mInPlace = false;
mActivationCount = 0;
mFeedState = EFeedState.DOWN;
}
return;
} // end of unsubscribe()
/**
* Forwards the request to all matching repliers, returning
* the request instance. Throws {@code IllegalStateException}
* if no matching repliers are found and, consequently, no
* replies will be received. Use the returned
* {@link ERequest} instance to query the request state or
* cancel the request.
*
* Note: once the {@link ERequest} is
* returned, the application is responsible for tracking
* all active request instances. This request feed does
* not store or track requests on behalf of the
* application. {@link #close() Closing} this requests feed
* does not automatically cancel active requests created by
* this feed. The application is responsible for canceling
* active requests.
*
* @param msg send this request message to the repliers.
* @return the resulting request.
* @throws NullPointerException
* if {@code msg} is {@code null}.
* @throws IllegalArgumentException
* if {@code msg} does not match the request subject key.
* @throws IllegalStateException
* if there are currently no repliers for this request.
*
* @see ERequestFeed.ERequest#close()
*/
public ERequest request(final ERequestMessage msg)
{
// Validate parameters.
Objects.requireNonNull(msg, "msg is null");
// Is this subject correct?
if (!(msg.key()).equals(mSubject.key()))
{
throw (
new IllegalArgumentException(
String.format(
"received msg key %s, expected %s",
msg.key(),
mSubject.key())));
}
// Is this feed still alive?
if (!mIsActive.get())
{
throw (new IllegalStateException("feed is closed"));
}
// Is the feed subscription in place?
if (!mInPlace)
{
throw (new IllegalStateException("not subscribed"));
}
return (doRequest(msg));
} // end of request(ERequestMessage)
/**
* Finds the repliers matching the request message and
* returns the request generated for the request process. The
* caller is expected to verify the request message and feed
* state prior to calling this method. This method is
* provided since
* {@link EMultiRequestFeed#request(ERequestMessage)}
* performs the same checks as
* {@link ERequestFeed#request(ERequestMessage)}.
* @param msg request message.
* @return the resulting request.
*/
/* package */ ERequest doRequest(final ERequestMessage msg)
{
// Are there any active repliers?
if (mActivationCount == 0)
{
// No.
throw (
new IllegalStateException(
"no repliers for request"));
}
// Everything checks out. Send the request on its way.
final Map repliers =
new HashMap<>(mRepliers.size());
EReplyFeed.ERequest replyRequest;
int replierCount = 0;
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s request %d: forwarding request to %,d repliers.",
mEClient.location(),
mFeedId,
mRepliers.size()));
}
// Create the request first, then update its
// repliers.
final ERequest retval =
new ERequestFeed.ERequest(this, mReplyCallbacks);
// Send the request message to each replier together
// with an ERequest instance and collect the
// returned reply feed request instance.
for (EReplyFeed replier : mRepliers)
{
replyRequest = replier.request(retval, msg);
// Check if the reply request is not null 'cuz
// null is returned if the replier is either
// inactive or did not accept the request.
if (replyRequest != null)
{
replierCount += replyRequest.remaining();
repliers.put(replier, replyRequest);
}
}
// Are there any repliers for this request.
if (replierCount > 0)
{
// Yes. Update the request with the repliers and
// reply count.
retval.repliers(replierCount, repliers.values());
// Have the replier dispatch the request *after*
// the EReplyFeed.ERequest is stored away.
// Otherwise, there is a chance the reply will
// arrive before replierCount is set.
repliers.entrySet()
.stream()
.forEach(
entry ->
(entry.getKey()).dispatch(
entry.getValue()));
}
else
{
// No. Let the user know that this request went no
// where.
// Be sure to close the request before throwing up.
retval.close();
throw (
new IllegalStateException(
"no repliers for request"));
}
return (retval);
} // end of doRequest(ERequestMessage)
/**
* Adds the reply feed to the replier list if the
* replier's location matches this requestor's scope.
*
* Note: the caller has already determine that the replier
* scope is not {@link FeedScope#REMOTE_ONLY}.
*
* @param location replier location.
* @param feed add this reply feed if capable
*/
/* package */ synchronized void
addReplier(final ClientLocation location,
final EReplyFeed feed)
{
// Add the replier if the replier's local and scope
// matches this local requestor's scope.
// Note: the caller has already determined that the
// replier scope is *not* REMOTE_ONLY.
if ((mScope == FeedScope.LOCAL_ONLY &&
location == ClientLocation.LOCAL) ||
mScope == FeedScope.LOCAL_AND_REMOTE ||
(mScope == FeedScope.REMOTE_ONLY &&
location == ClientLocation.REMOTE))
{
// Add the feed to the list.
mRepliers.add(feed);
// If the replier feed state is up, then update the
// activation count.
if (feed.feedState() == EFeedState.UP)
{
++mActivationCount;
// Is this the first active replier?
if (mActivationCount == 1)
{
// Yes. Inform the requestors that this
// feed is up.
mFeedState = EFeedState.UP;
mEClient.dispatch(
new FeedStatusTask(
mFeedState, this, mStatusCallback));
}
}
}
return;
} // end of addReplier(ClientLocation, EReplyFeed)
/**
* Removes a reply feed from the repliers list.
* @param feed remove this reply feed.
*/
/* package */ synchronized void
removeReplier(final EReplyFeed feed)
{
// Remove the replier from the feed list even though the
// activation count may be zero. That is because the
// replier may be in the list but its feed state is down.
// Make sure the replier's feed state is update before
// decrementing the activation count.
if (mRepliers.remove(feed) &&
feed.feedState() == EFeedState.UP &&
mActivationCount > 0)
{
// Removing
--mActivationCount;
// If the feed no longer has any repliers, then
// inform the requestor client of that fact.
if (mActivationCount == 0)
{
mFeedState = EFeedState.DOWN;
mEClient.dispatch(
new FeedStatusTask(
mFeedState, this, mStatusCallback));
}
}
return;
} // end of removeReplier(EReplyFeed)
/**
* Creates a new request feed for the given client, message
* key, and feed scope. The feed is assigned a unique
* identifier within the client's scope.
*
* This method does not parameter validation since this is
* a {@code package private} method.
*
* @param cl the eBus requestor opening this feed.
* @param key the request message key.
* @param scope whether this request matches local repliers,
* remote repliers, or both.
* @param l {@code client} location.
* @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 opened request feed.
*/
public static ERequestFeed open(final ERequestor cl,
final EMessageKey key,
final FeedScope scope,
final ClientLocation l,
final boolean isMulti)
{
final EClient eClient;
final ERequestSubject subject;
final Map, ReplyCallback> cbs =
createReplyCallbacks(key.messageClass());
final ERequestFeed retval;
// Find or open the eBus client wrapping client.
eClient = EClient.findOrCreateClient(cl, l);
subject = ERequestSubject.findOrCreate(key);
retval = new ERequestFeed(eClient,
scope,
subject,
cbs);
// 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);
}
// Note: request client will be informed about the feed
// state when subscribing. Do NOT send a feed status
// update now.
return (retval);
} // end of open(...)
/**
* Returns a reply callback map containing all the reply
* message keys but with {@code null} callbacks. These
* callbacks will either be set by the application or
* defaulted to
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}
* interface method.
* @param key request message key.
* @return reply callback map.
*/
/* package */ static Map, ReplyCallback>
createReplyCallbacks(final Class extends EMessage> mc)
{
final MessageType mt =
(MessageType) DataType.findType(mc);
final List> replyClasses =
mt.replyTypes();
final Map, ReplyCallback> retval =
new HashMap<>(replyClasses.size());
replyClasses.forEach(clazz -> retval.put(clazz, null));
return (retval);
} // end of createReplyCallbacks(Class)
} // end of class ERequestFeed