net.sf.eBus.client.ERequestFeed Maven / Gradle / Ivy
//
// Copyright 2015, 2016, 2019 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package 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 net.sf.eBus.client.EClient.ClientLocation;
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;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@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:
* Use {@link ERequestFeed.Builder} to create a request feed
* instance for a given {@code ERequestor} instance and
* {@link EMessageKey type+topic message key}.
*
*
* Use {@link Builder#statusCallback(FeedStatusCallback)},
* {@link Builder#replyCallback(ReplyCallback)} and/or
* {@link Builder#replyCallback(java.lang.Class, ReplyCallback)}
* to set Java lambda expression used in place of
* {@link ERequestor} interface methods.
*
*
* Step 3:
* {@link #subscribe() Subscribe} to reply message key.
*
*
* Step 4:
* {@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 5: 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 6: 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 7: When
* requestor is shutting down, {@link #unsubscribe() retract} the
* subscription and {@link #close close} the feed.
*
* Example use of 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 {
private final EMessageKey mKey;
private final FeedScope mScope;
private final List<ERequestFeed.ERequest> mRequests;
private ERequestFeed mFeed;
public CatalogRequestor(final String subject, final FeedScope scope) {
mKey = new EMessageKey(com.acme.CatalogOrder.class, subject);
mScope = scope;
mRequests = new ArrayList<>();
}
@Override
public void startup() {
try {
Step 2: Open request feed. May override ERequestor interface methods.
mFeed = (ERequestFeed.builder()).target(this)
.messageKey(mKey)
.scope(mScope)
.builder();
Step 3: 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 6: Cancel request by closing request.
request.close();
}
mRequests.clear();
}
Step 7: 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 4: 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 = (CatalogOrder.builder()).subject(mKey.subject())
.timestamp(Instant.now())
.product(product)
.price(price)
.shipping(shipping)
.address(address)
.build();
try {
synchronized (mRequests) {
mRequests.add(mFeed.request(msg));
}
} catch (Exception jex) {
// Request failed. Put recovery code here.
}
}
}
*
* @see ERequestor
* @see EReplier
* @see EReplyFeed
*
* @author Charles Rapp
*/
public final class ERequestFeed
extends ESingleFeed
implements IERequestFeed
{
//---------------------------------------------------------------
// 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, IERequestFeed)}
* 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 =
LoggerFactory.getLogger(ERequestFeed.class);
//-----------------------------------------------------------
// 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 final 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;
/**
* Set to {@code true} if {@link ERequest#close()} may
* be called and {@code false} if not.
*/
private final boolean mCloseFlag;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new request feed based on builder settings.
* @param builder contains request feed settings.
*/
private ERequestFeed(final Builder builder)
{
super (builder);
mRepliers = new ArrayList<>();
mStatusCallback = builder.mStatusCallback;
mReplyCallbacks = builder.mReplyCallbacks;
mCloseFlag = builder.mCloseFlag;
} // end of ERequestFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EFeed Interface Implementation.
//
/**
* Cancels this feed's active requests
*/
@Override
protected synchronized void inactivate()
{
((ERequestSubject) mSubject).unsubscribe(this);
} // 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;
sLogger.debug(
"{} requestor {}, feed {}: setting {} feed state to {} ({}).",
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.
sLogger.trace(
"{} requestor {}, feed {}: {} feed state={}, activation count={} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
fs,
mActivationCount,
mScope);
return (retval);
} // end of updateActivation(ClientLocation, EFeedState)
//
// end of EFeed Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if a request may be unilaterally
* canceled by calling {@link ERequest#cancel()} and
* {@code false} if not.
* @return {@code true} if requests may be unilaterally
* canceled.
*/
public boolean mayClose()
{
return (mCloseFlag);
} // end of mayClose()
//
// end of Get Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* 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)
*/
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));
} // 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.
*/
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);
} // 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);
} // end of replyCallbacks(Map<>)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Returns a new {@code ERequestFeed} builder instance.
* This instance should be used to build a single
* request feed instance and not used to create multiple
* such feeds.
* @return new feed builder.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
/**
* 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);
sLogger.debug(
"{} requestor {}, feed {}: subscribing to {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
mReplyCallbacks.entrySet()
.stream()
.filter(
entry ->
(entry.getValue() == null))
.forEachOrdered(
entry ->
{
// Did the client override
// generic 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.
} // 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.
sLogger.debug(
"{} requestor {}, feed {}: unsubscribing from {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
((ERequestSubject) mSubject).unsubscribe(this);
mInPlace = false;
mActivationCount = 0;
mFeedState = EFeedState.DOWN;
}
} // 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 the message class assignable from the key message
// class?
// Is this subject correct?
if (!(mSubject.key()).isAssignableFrom(msg.key()))
{
throw (
new IllegalArgumentException(
String.format(
"received msg key {}, expected {}",
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;
sLogger.debug(
"{} request {}: forwarding request to {} repliers.",
mEClient.location(),
mFeedId,
mRepliers.size());
// Create the request first, then update its
// repliers.
final ERequest retval =
new ERequestFeed.ERequest(this,
mReplyCallbacks,
mCloseFlag);
// 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()));
ESubject.exhaust(msg);
}
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
*/
@SuppressWarnings ({"java:S1067"})
/* 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));
}
}
}
} // 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));
}
}
} // end of removeReplier(EReplyFeed)
/**
* 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 MessageType mt)
{
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)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EREquestFeed.Builder} is now the preferred
* mechanism for creating a {@link ERequestFeed} instance. A
* {@code Builder} instance is acquired from
* {@link ERequestFeed#builder()}. The following example
* shows how to create an {@code ERequestFeed} instance using
* a {@code Builder}. The code assumes that the class
* implements {@code ERequestor} interface {@code feedStatus}
* method but uses other methods for reply messages. The
* example {@code CatalogOrder} request message supports two
* replies besides the generic {@code EReplyMessage}:
* {@code OrderRejectReply} and {@code OrderUpdateReply}.
* @Override public void startup() {
final EMessageKey key = new EMessageKey(com.acme.CatalogOrder.class, subject);
final ERequestFeed feed = (ERequestFeed.builder()).target(this)
.messageKey(key) Message key must be set prior to setting reply callbacks.
.scope(EFeed.FeedScope.REMOTE_ONLY)
// Call .statusCallback(lambda expression) to replace feedStatus method
// Set different callback for each reply message class.
.replyCallback(EReplyMessage.class, this::genericReply)
.replyCallback(OrderRejectReply.class, this::orderReject)
.replyCallback(OrderUpdateReply.class, this::orderUpdate)
.build();
...
}
*
* @see #builder()
*/
public static final class Builder
extends ESingleFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Set to {@code true} if {@link ERequest#close()} may
* be called and {@code false} if not.
*
* Defaults to {@code true}.
*
*/
private boolean mCloseFlag;
/**
* 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 Map, ReplyCallback> mReplyCallbacks;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private Builder()
{
super (FeedType.REQUEST_FEED, ERequestFeed.class);
mCloseFlag = true;
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Returns eBus subject for the configured message key.
* @return eBus feed subject.
*/
@Override
protected ESubject getSubject()
{
return (ERequestSubject.findOrCreate(mKey));
} // end of getSubject()
@Override
protected Validator validate(final Validator problems)
{
final boolean replyOverride =
isOverridden(REPLY_METHOD,
int.class,
EReplyMessage.class,
ERequestFeed.ERequest.class);
// If callbacks not explicitly set, then set them to
// the defaults.
if (mStatusCallback == null &&
mTarget != null &&
isOverridden(FEED_STATUS_METHOD,
EFeedState.class,
IERequestFeed.class))
{
mStatusCallback =
((ERequestor) mTarget)::feedStatus;
}
if (replyOverride && mTarget != null)
{
final ReplyCallback replyCb =
((ERequestor) mTarget)::reply;
mReplyCallbacks.entrySet()
.stream()
.filter(e -> e.getValue() == null)
.forEachOrdered(
e -> e.setValue(replyCb));
}
super.validate(problems)
.requireTrue((mKey != null &&
mKey.isRequest()),
"messageKey",
"messageKey is not a request")
.requireTrue((!mKey.isLocalOnly() ||
mScope == FeedScope.LOCAL_ONLY),
"messageKey",
String.format(
"%s is local-only but feed scope is %s",
mKey,
mScope))
.requireNotNull(mStatusCallback,
"statusCallback");
checkReplyCallbacks(problems);
return (problems);
} // end of validate(Validator)
/**
* Returns a new {@code ERequrestFeed} instance based on
* {@code this Builder}'s configuration.
* @return new request feed.
*/
@Override
protected ERequestFeed buildImpl()
{
return (new ERequestFeed(this));
} // end of buildImpl()
/**
* Returns {@code this} reference.
* @return {@code this} reference.
*/
@Override
protected Builder self()
{
return (this);
} // end of self()
//
// end of Abstact Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets eBus message key defining this feed. Returns
* {@code this Builder} instance so that configuration
* methods may be chained.
* @param key eBus message key.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code target} is {@code null}.
*/
@Override
public Builder messageKey(final EMessageKey key)
{
// Throws NullPointerException if key is null.
super.messageKey(key);
// Message key is set. Now fill in the reply callback
// map.
mReplyCallbacks =
createReplyCallbacks(
(MessageType) DataType.findType(
key.messageClass()));
return (this);
} // end of messageKey(EMessageKey)
/**
* Sets "may close" flag to given value. A {@code true}
* means requester has ability to close an active
* request; {@code false} means requester may
* not cancel an active request and must wait
* for the request to complete.
*
* Defaults to {@code true}.
*
* @param flag {@code true} means request may be closed.
* @return {@code this Builder} instance.
*/
public Builder mayClose(final boolean flag)
{
mCloseFlag = flag;
return (self());
} // end of mayClose(boolean)
/**
* 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, IERequestFeed)}.
* A {@code null cb} results in feed status updates
* posted to the
* {@link ERequestor#feedStatus(EFeedState, IERequestFeed)}
* override.
*
* The following example shows how to use this method:
*
* statusCallback(
(fs, f) →
{
if (fs == EFeedState.DOWN) {
// Clean up in-progress work.
}
}
* @param cb feed status update callback. May be
* {@code null}.
* @return {@code this Builder} instance.
*/
public Builder statusCallback(final FeedStatusCallback cb)
{
mStatusCallback = cb;
return (this);
} // end of statusCallback(FeedStatusCallback<>)
/**
* 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.
*
*
* The following example shows how to use this method
* to set a reply callback lambda expression:
*
* {@code OrderRejectReply.class, this::orderReject}
* @param mc the reply message class.
* @param cb callback for the reply message.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code mc} is {@code null}.
* @throws IllegalArgumentException
* if {@code mc} is not a reply for this request.
* @throws IllegalStateException
* if {@link #messageKey(EMessageKey)} was not
* successfully called prior to setting the reply
* callback.
*
* @see #messageKey(EMessageKey)
* @see #replyCallback(ReplyCallback)
*/
public Builder replyCallback(final Class extends EReplyMessage> mc,
final ReplyCallback cb)
{
// Has the message key been set?
if (mKey == null)
{
throw (
new IllegalStateException(
"messageKey must be set adding replyCallback"));
}
Objects.requireNonNull(mc, "mc is null");
if (!mReplyCallbacks.containsKey(mc))
{
throw (
new IllegalArgumentException(
mc.getSimpleName() +
" is not a " +
mKey +
" reply"));
}
mReplyCallbacks.put(mc, cb);
return (this);
} // end of replyCallback(Class, ReplyCallback)
/**
* Sets reply callback for all message classes. This
* will override any previous calls to
* {@link #replyCallback(Class, ReplyCallback)} will be
* overwritten by this method. Therefore this method
* should be called prior to setting specific reply
* message class callbacks.
*
* 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)}.
*
*
* If the goal is to set a callback for a specific reply
* message class, then use
* {@link #replyCallback(Class, ReplyCallback)}.
*
*
* The following example shows how to set a generic
* reply callback lambda expression:
*
* {@code replyCallback(this::orderReply)}
* @param cb callback for the reply message.
* @return {@code this Builder} instance.
* @throws IllegalStateException
* if {@link #messageKey(EMessageKey)} was not
* successfully called prior to setting the reply
* callback.
*
* @see #replyCallback(Class, ReplyCallback)
*/
public Builder replyCallback(final ReplyCallback cb)
{
// Has the message key been set?
if (mKey == null)
{
throw (
new IllegalStateException(
"messageKey must be set adding replyCallback"));
}
for (Map.Entry, ReplyCallback> e :
mReplyCallbacks.entrySet())
{
e.setValue(cb);
}
return (this);
} // end of replyCallback(ReplyCallback)
//
// end of Set Methods.
//-------------------------------------------------------
/**
* Checks if all possible reply callbacks are set.
* Missing callbacks are reported in {@code problems}.
* @param problems place reply callback problems into
* this list.
*/
private void checkReplyCallbacks(final Validator problems)
{
final boolean replyOverride =
isOverridden(REPLY_METHOD,
int.class,
EReplyMessage.class,
ERequestFeed.ERequest.class);
mReplyCallbacks.entrySet()
.stream()
.filter(e -> (e.getValue() == null))
.forEachOrdered(
entry ->
{
// Did the client override
// generic reply?
if (!replyOverride)
{
// Nope.
problems.addError(
new MultiKey2<>(
"replyCallback",
"missing " +
entry.getKey() +
" callback"));
}
});
} // end of checkReplyCallbacks(Validator)
} // end of class Builder
/**
* 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;
/**
* Set to {@code true} if {@link ERequest#close()} may
* be called and {@code false} if not.
*/
private final boolean mCloseFlag;
/**
* 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.
* @param closeFlag {@code false} if {@link #close()} may
* not be called.
* @param cxlReq non-{@code null} if this request can be
* canceled via an asynchronous message.
*/
private ERequest(final ERequestFeed feed,
final Map, ReplyCallback> cbs,
final boolean closeFlag)
{
super (feed.mEClient,
feed.mScope,
feed.mFeedType,
feed.mSubject);
mRepliers = new ArrayList<>();
mRemaining = 0;
mRequestState = RequestState.NOT_PLACED;
mReplyCallbacks = cbs;
mCloseFlag = closeFlag;
} // end of ERequest(ERequestFeed, Map<>, boolean)
//
// 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.forEach(req -> req.close());
// Clear out the repliers list and mark this
// request as canceled.
mRepliers.clear();
setState(RequestState.CANCELED);
}
} // end of inactivate()
@Override
/* package */ int updateActivation(final ClientLocation loc,
final EFeedState fs)
{
return (0);
} // end of updateActivation(ClientLocation, EFeedState)
/**
* Cancels this request immediately and unilaterally if
* this action is allowed. If not allowed then an
* {@code IllegalStateException} is thrown.
*
* Does nothing if this request is not active.
*
*
* If this request may not be unilaterally canceled then
* it may allow for a cancel request message to
* be sent to the replier(s). If all repliers accept the
* cancel request then this request is then canceled. But
* if at least one replier rejects the cancel request
* then the request is still active.
*
* @throws IllegalStateException
* if either this request may not be directly canceled.
*/
@Override
public void close()
{
// Is this request still alive?
if (mRequestState == RequestState.ACTIVE)
{
// Yes. But may it be unilaterally closed?
if (!mCloseFlag)
{
throw (
new IllegalStateException(
"request may not be canceled"));
}
// Clear to go ahead and close this request.
super.close();
}
} // end of close()
//
// 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()
/**
* Returns {@code true} if a request may be unilaterally
* canceled by calling {@link ERequest#cancel()} and
* {@code false} if not.
* @return {@code true} if requests may be unilaterally
* canceled.
*/
public boolean mayClose()
{
return (mCloseFlag);
} // end of mayClose()
//
// 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);
sLogger.debug(
"{}: {} remaining (active={}, state={})",
this,
mRemaining,
mIsActive.get(),
mRequestState);
} // end of repliers(int, Collection<>)
//
// end of Set Methods.
//-------------------------------------------------------
/**
* Attempts to cancel this request with extant repliers.
* Unlike {@link #close()} a replier may reject this
* attempt at canceling the request thus keeping the
* request alive. This method of terminating a request
* should be preferred over {@code close()} since it
* allows the replier to respond to the cancel request.
* @throws IllegalStateException
* if this request is not active.
*/
public void cancel()
{
// Is this request still alive?
if (mRequestState != RequestState.ACTIVE)
{
throw (
new IllegalStateException(
"request is not active"));
}
// Yes, forward the cancel request to all repliers.
mRepliers.forEach(req -> req.cancel());
} // end of cancel()
/**
* 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.
*/
@SuppressWarnings ("unchecked")
/* package */ void reply(final int remaining,
final EReplyMessage msg,
final EReplyFeed.ERequest replier)
{
final boolean isActive = mIsActive.get();
sLogger.debug(
"{}: {} reply, {} final, {} remaining (active={}, state={})",
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)
{
final Class extends EReplyMessage> mc =
(Class extends EReplyMessage>)
(msg.key()).messageClass();
// 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,
findCallback(mc)));
ESubject.exhaust(msg);
}
} // 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);
sLogger.debug(
"{}: {} remaining (active={}, state={})",
this,
mRemaining,
mIsActive.get(),
mRequestState);
} // 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)
{
sLogger.trace("{}: {} -> {}.",
this,
mRequestState,
nextState);
mRequestState = nextState;
} // end of setState(RequestState)
/**
* Returns the reply callback associated with the given
* message class.
* @param mc find reply callback for this message class.
* @return reply callback.
*/
private ReplyCallback findCallback(final Class extends EReplyMessage> mc)
{
ReplyCallback retval = null;
// Is this message class already in the reply
// callback map?
if (mReplyCallbacks.containsKey(mc))
{
// Yes. Use that entry.
retval = mReplyCallbacks.get(mc);
}
// No. Look for the reply callback entry closest to
// the target message class.
else
{
Class extends EReplyMessage> bc = null;
int distance = Integer.MAX_VALUE;
int cd;
// Look for reply callback matching the message
// class and within the eBus distance.
for (Class extends EReplyMessage> key : mReplyCallbacks.keySet())
{
cd = subclassDistance(mc, key);
if (cd >= 0 && cd < distance)
{
distance = cd;
bc = key;
}
}
if (bc != null)
{
retval = mReplyCallbacks.get(bc);
mReplyCallbacks.put(bc, retval);
}
}
return (retval);
} // end of findCallback(Class)
} // 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 IERequestFeed 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();
sLogger.trace("{} executing", this);
if (target != null)
{
try
{
mCallback.call(
mFeedState, (IERequestFeed) mFeed);
}
catch (Throwable tex)
{
final String reason = "{} exception";
final String className =
(target.getClass()).getName();
if (sLogger.isDebugEnabled())
{
sLogger.warn(reason, className, tex);
}
else
{
sLogger.warn(reason, className);
}
}
}
} // 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();
sLogger.trace("{}: running.", this);
if (target != null)
{
try
{
mCallback.call(
mRemaining, mMessage, mRequest);
}
catch (Throwable tex)
{
final String reason =
"ReplyTask[{}, {}] exception";
final String className =
(target.getClass()).getName();
final EMessageKey key = mMessage.key();
if (sLogger.isDebugEnabled())
{
sLogger.warn(reason, className, key, tex);
}
else
{
sLogger.warn(reason, className, key);
}
}
}
// 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();
}
} // 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
} // end of class ERequestFeed