net.sf.eBus.client.EReplyFeed Maven / Gradle / Ivy
//
// Copyright 2015, 2016, 2019, 2020 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.List;
import java.util.Objects;
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;
import net.sf.eBus.util.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@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 EReplyFeed.Builder Build}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.
*
*
* Use
* {@link Builder#cancelRequestCallback(CancelRequestCallback)}
* and
* {@link Builder#requestCallback(RequestCallback)} to set Java
* lambda expressions used in place of {@link EReplier} interface
* methods.
*
* Step 3: {@link #advertise() Advertise} this
* replier to eBus. This allows eBus to match repliers with
* requestors.
*
*
* Step 4: 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 5:
* {@link EReplyFeed.ERequest#reply(EReplyMessage) Send} one or
* more {@link EReplyMessage reply} messages back to the
* requestor.
*
*
* Step 6: When the replier is shutting down,
* {@link #unadvertise() retract} the reply advertisement and
* {@link #close() close} the feed.
*
* Example use of 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 {
private final EMessageKey mKey;
private final FeedScope mScope;
private EReplyFeed mFeed;
private final List<EReplyFeed.ERequest> mRequests;
public CatalogReplier(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 a reply feed for reply message key. May override EReplier interfaces methods.
// This advertisement has no associated ECondition and uses EReplier interface method overrides.
mFeed = (EReplyFeed.builder()).target(this)
.messageKey(mKey)
.scope(mScope)
.build();
Step 3: Advertise reply feed.
mFeed.advertise();
mFeed.updateFeedState(EFeedState.UP);
} catch (IllegalArgumentException argex) {
// Advertisement failed. Place recovery code here.
}
}
Step 4: 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 boolean mayRespond) {
// 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 5: Send one or more reply messages back to requestor.
public void orderReply(final EReplyFeed.ERequest request, final ReplyStatus status, final String reason) {
final ERequestAd ad = mRequests.get(request);
if (mRequests.contains(request) && request.isActive()) {
request.reply((OrderReply.builder()).subject(mKey.subject())
.timestamp(Instant.now())
.replyStatus(status)
.replyReason(reason)
.build());
// If the request processing is complete, remove the request.
if (status.isFinal()) {
mRequests.remove(request);
}
}
}
@Override
public void shutdown() {
final String subject = mKey.subject();
// While eBus will does this for us, it is better to do it ourselves.
for (EReplyFeed.ERequest request : mRequests) {
request.reply((EReplyMessage.builder()).subject(subject)
.timestamp(Instant.now())
.replyStatus(ReplyStatus.ERROR)
.replyReason("shutting down")
.build());
}
mRequests.clear();
Step 6: When shutting down, either unadvertise or close reply feed.
if (mFeed != null) {
mFeed.close();
mFeed = null;
}
}
}
*
* @see EReplier
* @see ERequestor
* @see ERequestFeed
*
* @author Charles Rapp
*/
public final class EReplyFeed
extends ESingleFeed
implements IEReplyFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link EReplier#request(EReplyFeed.ERequest)} method name.
*/
/* package */ static final String REQUEST_METHOD = "request";
/**
* {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}
* method name.
*/
/* package */ static final String CANCEL_METHOD =
"cancelRequest";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(EReplyFeed.class);
//-----------------------------------------------------------
// 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 final RequestCallback mRequestCallback;
/**
* Contains the functional interface callback for request
* messages. If not explicitly set by client, then defaults
* to
* {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}.
*/
private final CancelRequestCallback mCancelCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new reply feed based on the builder
* configuration. Note that the builder configuration is
* guaranteed to be valid prior to calling this constructor.
* @param builder contains valid reply feed configuration.
*/
private EReplyFeed(final Builder builder)
{
super (builder);
mCondition = builder.mCondition;
mRequests = new ArrayList<>();
mDataType = builder.getMessageType();
mRequestCallback = builder.mRequestCallback;
mCancelCallback = builder.mCancelCallback;
} // end of EReplyFeed(Builder)
//
// 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");
}
} // 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.
//
/**
* 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;
sLogger.debug(
"{} replier {}, feed {}: setting {} feed state to {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
update,
mScope);
// Forward the update to the subject.
((ERequestSubject) mSubject).updateFeedState(this);
}
// No change. Nothing to do.
} // end of updateFeedState(EFeedState)
/**
* Forwards the given request message and request object to
* the client, returning a {@link ERequest} object. Note that
* {@code 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}.
*/
/* 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,
mCondition,
mCancelCallback);
synchronized (mRequests)
{
mRequests.add(retval);
}
}
return (retval);
} // end of request(ERequest, ERequestMessage)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Returns a new {@code EReplyFeed} builder instance. This
* instance should be used to build a single reply feed
* instance and not used to create multiple such feeds.
* @return new reply feed builder.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
/**
* 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)
{
sLogger.debug(
"{} replier {}, Feed {}: advertising {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
((ERequestSubject) mSubject).advertise(this);
mInPlace = true;
}
} // 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)
{
sLogger.debug(
"{} replier {}, Feed {}: unadvertising ({}).",
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");
}
} // 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));
} // 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);
}
} // 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(mRequests);
mRequests.clear();
}
requests.forEach(
request ->
{
final EReplyMessage.Builder, ?> builder =
EReplyMessage.builder();
request.reply(builder.subject((request.key()).subject())
.replyStatus(ReplyStatus.ERROR)
.replyReason(reason)
.build());
});
} // end of failRequests(String)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EReplyFeed.Builder} is now the preferred
* mechanism for creating a {@code EReplyFeed} instance.
* A {@code Builder} instance is acquired from
* {@link EReplyFeed#builder()}. The following example shows
* how to create a {@code EReplyFeed} instance using a
* {@code Builder}. The code shows how {@code EReplier}
* interface method
* {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}
* and
* {@link EReplier#request(EReplyFeed.ERequest)} methods may
* be replaced with lambda expressions.
* @Override public void startup() {
final EMessageKey key = new EMessageKey(com.acme.CatalogOrder.class, subject);
final EReplyFeed feed = (EReplyFeed.builder()).target(this)
.messageKey(key)
.scope(EFeed.FeedScope.REMOTE_ONLY)
.condition(m → ((CatalogOrder) m).category == Category.APPLIANCES)
.cancelRequest(this::cancelOrder)
.requestCallback(this::newOrder)
.build();
...
}
*
* @see #builder()
*/
public static final class Builder
extends ESingleFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Use this condition to check if the request message
* should be forwarded to replier. Default setting is
* {@link #NO_CONDITION}.
*/
private ECondition mCondition;
/**
* 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, boolean)}.
*/
private CancelRequestCallback mCancelCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Private method since a {@code Builder} instance may
* only be instantiated by {@link #builder()}.
*
* @see #builder()
*/
private Builder()
{
super (FeedType.REPLY_FEED, EReplyFeed.class);
mCondition = NO_CONDITION;
} // 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()
/**
* Checks that both request and cancel request callbacks
* are defined.
* @param problems place invalid configuration settings
* in this problems list.
* @return {@code problems} to allow for method chaining.
*/
@Override
protected Validator validate(final Validator problems)
{
// If the callbacks were not put in place, then use
// the defaults.
if (mRequestCallback == null &&
mTarget != null &&
isOverridden(REQUEST_METHOD,
EReplyFeed.ERequest.class))
{
mRequestCallback =
((EReplier) mTarget)::request;
}
if (mCancelCallback == null &&
mTarget != null &&
isOverridden(CANCEL_METHOD,
EReplyFeed.ERequest.class,
boolean.class))
{
mCancelCallback =
((EReplier) mTarget)::cancelRequest;
}
return (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(mRequestCallback,
"requestCallback")
.requireNotNull(mCancelCallback,
"cancelCallback"));
} // end of validate(Validator)
/**
* Returns a new {@code EReplyFeed} instance based on
* {@code this Builder}'s configuration.
* @return new reply feed.
*/
@Override
protected EReplyFeed buildImpl()
{
return (new EReplyFeed(this));
} // end of buildImpl()
/**
* Returns {@code this} reference.
* @return {@code this} reference.
*/
@Override
protected Builder self()
{
return (this);
} // end of self()
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns message type associated with the configured
* message key.
* @return request message type.
*/
private MessageType getMessageType()
{
return (
(MessageType) DataType.findType(
mKey.messageClass()));
} // end of getMessageType()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets advertisement condition to the given value. May
* be {@code null} which results in condition being set
* to {@link #NO_CONDITION}.
*
* An example using this method is:
*
* {@code condition(m -> ((CatalogUpdate) m).category == Category.APPLIANCES)}
* @param condition advertisement condition.
* @return {@code this Builder} instance.
*/
public Builder condition(final ECondition condition)
{
if (condition == null)
{
mCondition = NO_CONDITION;
}
else
{
mCondition = condition;
}
return (this);
} // end of condition(ECondition)
/**
* 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.
*
* An example using this method is:
*
* {@code requestCallback(this::newOrder)}
* @param cb the request callback. May be {@code null}.
* @return {@code this Builder} instance.
*/
public Builder requestCallback(final RequestCallback cb)
{
mRequestCallback = cb;
return (this);
} // 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, boolean)}.
* A {@code null cb} means that cancellations are passed
* to the
* {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}
* override.
*
* An example using this method is:
*
* {@code this::cancelOrder}
* @param cb the cancel request callback. May be
* {@code null}.
* @return {@code this Builder} instance.
*/
public Builder cancelRequestCallback(final CancelRequestCallback cb)
{
mCancelCallback = cb;
return (this);
} // end of cancelRequestCallback(CancelRequestCallback)
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class Builder
/**
* {@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 data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Post reply messages and cancel replies to this request
* feed. The requestor tracks the request state.
*/
private final ERequestFeed.ERequest mRequestor;
/**
* Mark replies as coming from this feed. The request
* feed needs to know from where replies originate.
*/
private final EReplyFeed mReplier;
/**
* The request message itself.
*/
private final ERequestMessage mRequest;
/**
* Apply {@link #mRequest} to this condition and deliver
* request if-and-only-if the message satisfies the
* condition.
*/
private final ECondition mCondition;
/**
* 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 mCancelCallback;
/**
* Tracks whether this request for its replier is active,
* cancel in-progress, or done. Initialize to active.
*/
private RequestState mRequestState;
/**
* 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 mRemaining;
//-----------------------------------------------------------
// 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 condition request condition.
* @param cb request cancellation callback.
*/
/* package */ ERequest(final ERequestFeed.ERequest reqFeed,
final EReplyFeed repFeed,
final ERequestMessage request,
final ECondition condition,
final CancelRequestCallback cb)
{
super (repFeed.mEClient,
repFeed.mScope,
repFeed.mFeedType,
repFeed.mSubject);
mRequestor = reqFeed;
mReplier = repFeed;
mRequest = request;
mCondition = condition;
mCancelCallback = cb;
mRequestState = RequestState.ACTIVE;
// Assume local until updated.
mRemaining = 1;
} // end of ERequest(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// EFeed Abstract Method Overrides.
//
/**
* Cancels this request if still active.
*/
@Override
protected void inactivate()
{
if (mRequestState == RequestState.ACTIVE)
{
state(RequestState.CANCELED);
// Post this request cancellation to the replier.
(mReplier.mEClient).dispatch(
new CancelRequestTask(
this, false, mReplier, mCancelCallback));
}
} // 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 ((mRequest.key()).toString());
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns the associated request message.
* @return request message.
*/
public ERequestMessage request()
{
return (mRequest);
} // end of request()
/**
* Returns associated request message condition.
* @return message condition.
*/
public ECondition condition()
{
return (mCondition);
} // end of condition()
/**
* Returns the current request state.
* @return the request state.
*/
public RequestState state()
{
return (mRequestState);
} // end of state()
/**
* Returns the associated reply feed.
* @return a reply feed.
*/
public EReplyFeed replier()
{
return (mReplier);
} // end of replier()
/**
* Returns the remote replier count for this request.
* @return number of remaining remote repliers.
*/
/* package */ int remaining()
{
return (mRemaining);
} // 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)
{
sLogger.debug("Reply {}.{}: setting state to {}.",
mReplier.feedId(),
mFeedId,
state);
mRequestState = 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)
{
mReplier.requestDone(this);
close();
}
} // 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)
{
sLogger.debug(
"Reply {}.{}: number of {} repliers remaining is {}.",
mReplier.feedId(),
mFeedId,
mReplier.location(),
n);
// Have the request feed update its remaining count.
mRequestor.updateRemaining(mRemaining, n);
mRemaining = n;
} // 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)
{
// 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 (!(mReplier.isValidReply(msg.key())))
{
// No, wrong message type.
throw (
new IllegalArgumentException(
(msg.key()).className() +
" is not a valid reply message class for " +
(mRequest.key()).className()));
}
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 (mRequestState == RequestState.DONE)
{
throw (
new IllegalStateException(
"request is done"));
}
// Everything checks out. Is this request finished?
if (msg.isFinal() && (mRemaining - 1) == 0)
{
// Yes. Mark that done.
--mRemaining;
state(RequestState.DONE);
}
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"Reply {}.{}: forwarding reply message:\n{}",
mReplier.feedId(),
mFeedId,
msg);
}
else
{
sLogger.debug(
"Reply {}.{}: forwarding reply message {}.",
mReplier.feedId(),
mFeedId,
msg.key());
}
// Now hand this reply off to the originating request
// feed and that feed will forward it to the client.
mRequestor.reply(mRemaining, msg, this);
} // end of reply(EReplyMessage)
/**
* Posts an respond-able cancel request to replier.
* @return {@code true} if the request is still active
* with this replier and {@code false} if not.
*/
/* package */ boolean cancel()
{
final boolean retcode =
(mRequestState == RequestState.ACTIVE);
if (retcode)
{
// Post this request cancellation to the replier.
(mReplier.mEClient).dispatch(
new CancelRequestTask(
this, true, mReplier, mCancelCallback));
}
return (retcode);
} // end of cancel()
/**
* 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 (mRemaining == 0)
{
state(RequestState.DONE);
}
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"Reply {}.{}: forwarding reply message:\n{}",
mReplier.feedId(),
mFeedId,
msg);
}
else
{
sLogger.debug("Reply {}.{}: forwarding {}.",
mReplier.feedId(),
mFeedId,
msg.key());
}
// Now hand this reply off to the originating request
// feed and that feed will forward it to the client.
mRequestor.reply(mRemaining, msg, this);
} // end of remoteReply(EReplyMessage)
//
// end of Local Client API.
//-------------------------------------------------------
} // 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 mCallback;
//---------------------------------------------------------------
// 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);
mCallback = 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();
sLogger.trace("{}", this);
if (target != null)
{
try
{
final EReplyFeed.ERequest request =
(EReplyFeed.ERequest) mFeed;
final ECondition condition =
request.condition();
final ERequestMessage message =
request.request();
// Is this replier interested in this
// request?
if (condition.test(message))
{
// Yes. Forward request message to
// replier.
mCallback.call(request);
}
}
catch (Throwable tex)
{
final String reason =
"RequestTask[{}, {}] exception";
final String className =
(target.getClass()).getName();
if (sLogger.isDebugEnabled())
{
sLogger.warn(
reason, className, mFeed, tex);
}
else
{
sLogger.warn(reason, className, mFeed);
}
}
}
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (String.format("RequestTask[{}]", 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, boolean)}.
*/
private static final class CancelRequestTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Canceling this request.
*/
private final EReplyFeed.ERequest mRequest;
/**
* Set to {@code true} if replier may respond to this
* cancel request and {@code false} if this is a
* unilateral request cancellation.
*/
private final boolean mMayRespond;
/**
* 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 respondFlag {@code true} if replier may respond
* to this request cancellation.
* @param feed the canceled request applies to this feed.
* @param cb the method callback.
*/
private CancelRequestTask(final EReplyFeed.ERequest request,
final boolean respondFlag,
final EReplyFeed feed,
final CancelRequestCallback cb)
{
super (feed);
mRequest = request;
mMayRespond = respondFlag;
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.
*/
@SuppressWarnings({"java:S1181"})
@Override
public void run()
{
final Object target = (mFeed.eClient()).target();
sLogger.trace("{}", this);
if (target != null)
{
try
{
mCallback.call(mRequest, mMayRespond);
}
catch (Throwable tex)
{
final String reason =
"EReplier.cancelRequest({}) exception";
if (sLogger.isDebugEnabled())
{
sLogger.warn(reason, mFeed, tex);
}
else
{
sLogger.warn(reason, mFeed);
}
}
}
} // 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
} // end of class EReplyFeed