net.sf.eBus.client.EMultiReplyFeed Maven / Gradle / Ivy
//
// Copyright 2017, 2023 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.Objects;
import static net.sf.eBus.client.EFeed.NO_CONDITION;
import static net.sf.eBus.client.EReplyFeed.CANCEL_METHOD;
import static net.sf.eBus.client.EReplyFeed.REQUEST_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.util.Validator;
/**
* This feed allows an {@link EReplier} to open one feed for a
* given request message class and multiple message subjects. It
* acts as a proxy between the replier and the individual,
* subordinate feeds. The replier interacts solely with the
* multi-subject reply feed and is unable to access underlying
* {@code EReplyFeed}s. The replier client opens, advertises,
* un-advertises, and closes the multi-subject feed. In turn, the
* multi-subject feed opens, advertises, un-advertises, and
* closes subordinate {@link EReplyFeed}s. But
* subordinate feeds issue
* {@link EReplier#request(EReplyFeed.ERequest)}
* callbacks to the {@code EReplier} registered with the
* multi-subject feed. The multi-subject feed does not callback
* to the replier client. If the client opens a large number of
* subordinate feeds, then the client must be prepared for a
* large number of callbacks.
*
* The subordinate feeds are selected by passing a reply message
* message class to
* {@link EMultiFeed.Builder#messageClass(java.lang.Class) EMultiReplyFeed.Builder.messageClass}
* and either a subject list to
* {@link EMultiFeed.Builder#subjects(java.util.List) EMultiReplyFeed.Builder.subject}
* or
* a regular express query to
* {@link EMultiFeed.Builder#query(net.sf.eBus.util.regex.Pattern) EMultiReplyFeed.Builder.query}.
* The first limits the subordinate feeds to exactly those whose
* subject is listed. The second chooses subjects match the
* regular expression. In either case, the publisher may
* {@link #addFeed(String) add} or
* {@link #closeFeed(String) remove} feeds dynamically while the
* multi-subject feed is open. When adding a new reply feed, the
* new feed is configured in the same was as existing feeds and
* put into the same state (open, advertised, etc.).
*
* Example use of EMultiReplyFeed
* 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;
import net.sf.eBus.util.regex.Pattern;
Step 1: Implement EReplier interface.
public class CatalogReplier implements EReplier {
// Select subjects matching this query pattern.
private final Pattern mQuery;
private final FeedScope mScope;
private EMultiReplyFeed mFeed;
private final List<EReplyFeed.ERequest> mRequests;
public CatalogReplier(final Pattern query, final FeedScope scope) {
mQuery = query;
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 = (EMultiReplyFeed.builder()).target(this)
.scope(mScope)
.messageClass(com.acme.CatalogOrder.class)
.query(mQuery)
.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 EMultiRequestFeed
*
* @author Charles W. Rapp
*/
public final class EMultiReplyFeed
extends EMultiFeed
implements IEReplyFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Locals.
//
/**
* Use this condition to check if the request message should
* be forwarded to subscriber.
*/
private final ECondition mCondition;
/**
* Tracks the replier's ability to handle request messages.
* for this feed. {@link #mFeedState} tracks whether there
* are any requestor to this feed.
*/
private EFeedState mReplyState;
/**
* 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 multi-reply feed instance based on
* validated builder settings.
* @param builder contains multi-reply feed settings.
*/
private EMultiReplyFeed(final Builder builder)
{
super (builder);
mCondition = builder.mCondition;
mRequestCallback = builder.mRequestCallback;
mCancelCallback = builder.mCancelCallback;
mReplyState = EFeedState.UNKNOWN;
} // end of EMultiReplyFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IEReplyFeed Interface Implementations.
//
/**
* 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()
/**
* Advertises each subordinate {@link EReplyFeed}. If this
* feed is currently advertised, then does nothing. If the
* cancel request and request callbacks were previously set
* for this feed, then these callbacks are set in the
* subordinate feeds prior to advertising them.
* @throws IllegalStateException
* if this feed is closed or the client did not override
* {@link EReplier} methods nor put the required callbacks
* in place.
*
* @see #unadvertise()
* @see #updateFeedState(EFeedState)
* @see #close()
*/
@Override
public void advertise()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
if (!mInPlace)
{
sLogger.debug(
"{} multi-subject replier {}: advertising ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Advertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(EReplyFeed::advertise);
// This feed is now advertised.
mInPlace = true;
// If this multi-subject feed is query based, then
// listen for subject updates.
if (mQuery != null)
{
ESubject.addListener(this);
}
}
} // end of advertise()
/**
* Retracts this multi-subject replier feed by un-advertising
* each subordinate reply feed. Does nothing if this
* feed is not currently advertised.
* @throws IllegalStateException
* if this multi-subject reply feed is closed.
*
* @see #advertise()
* @see #close()
*/
@Override
public void unadvertise()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
if (mInPlace)
{
sLogger.debug(
"{} multi-subject replier {}: unadvertising ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Unadvertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(EReplyFeed::unadvertise);
// This feed is no longer advertised.
mReplyState = EFeedState.UNKNOWN;
mInPlace = false;
// Stop listening for subject updates.
ESubject.removeListener(this);
}
} // end of unadvertise()
/**
* Updates the reply feed state to the given value. If
* {@code update} equals the currently stored publish feed
* state, nothing is done. Otherwise, the updated value is
* stored and the subordinate reply feed states are updated
* as well.
*
* The reply feed state may be updated only when this feed
* is open and advertised. The method may not be called when
* the feed is closed or un-advertised.
*
* @param update the new reply feed state.
* @throws NullPointerException
* if {@code update} is {@code null}.
* @throws IllegalStateException
* if this feed was closed 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));
}
else if (!mInPlace)
{
throw (
new IllegalStateException(FEED_NOT_ADVERTISED));
}
// Does this update actually change anything?
else if (update != mReplyState)
{
// Yes. Apply the update.
mReplyState = update;
sLogger.debug(
"{} multi-subject replier {}: setting feed state to {} ({}).",
mEClient.location(),
mEClient.clientId(),
update,
mScope);
// Update each subordinate publish feed.
mFeeds.values()
.forEach(feed -> feed.updateFeedState(update));
}
} // end of updateFeedState(EFeedState)
//
// end of IEReplyFeed Interface Implementations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Returns a newly minted subordinate reply feed for the
* given key.
* @param key create feed for this key.
* @return a subordinate reply feed.
*/
@Override
protected EReplyFeed createFeed(EMessageKey key)
{
final EReplyFeed.Builder builder =
EReplyFeed.builder();
return (builder.target((EReplier) mEClient.target())
.location(location())
.messageKey(key)
.scope(mScope)
.condition(mCondition)
.requestCallback(mRequestCallback)
.cancelRequestCallback(mCancelCallback)
.build());
} // end of createFeed(EMessageKey)
/**
* Sets the feed status callback, advertises {@code feed},
* and updates the reply feed state. The reply feed state
* is taken from the most recent
* {@link #updateFeedState(EFeedState)} setting. If the
* reply feed state has never been updated, then the state
* is {@link EFeedState#UNKNOWN}.
* @param feed advertise this feed.
*/
@Override
protected void putFeedInPlace(final EReplyFeed feed)
{
feed.advertise();
feed.updateFeedState(mReplyState);
} // end of putFeedInPlace(EReplyFeed)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns the reply state which specifies whether this
* multi-subject reply feed (and its subordinate feeds) are
* ready to handle requests or not. An update reply state
* does not mean that the replier will receive requests but
* only that the replier is capable of handling requests at
* this time.
* @return current reply state.
*
* @see #feedState(String)
*/
public EFeedState replyState()
{
return (mReplyState);
} // end of replyState()
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Returns a new {@code EMultiReplyFeed} builder instance.
* This instance should be used to build a single reply feed
* instance and not used to create multiple such feeds.
* @return new feed builder.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EMultiReplyFeed.Builder} is the mechanism for
* creating an {@code EMultiReplyFeed} instance. A
* {@code Builder} instance is acquired from
* {@link EMultiReplyFeed#builder()}. The following example
* shows how to create an {@code EMultiReplyFeed} instance
* using a {@code Builder}. The code assumes that the target
* class implements {@code EReplier} interface methods.
* @Overricde public void startup() {
final EMultiReplyFeed feed = (EMultiReplyFeed.builder()).target(this)
.messageClass(com.acme.CatalogOrder.class)
.scope(EFeed.FeedScope.REMOTE_ONLY)
.subjects(mSubjectsList) // msSubjectsList is List<String> subject list
// Call .requestCallback(lambda expression) and
// .cancelRequestCallback(lambda expression) to replace request, cancelRequest methods
.build();
...
}
*/
public static final class Builder
extends EMultiFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Apply this condition to all incoming messages. Only
* those messages satisfying the condition are forwarded
* to the target. Defaults to {@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.
//
/**
* Creates a new multi-feed reply builder with the
* given "require callbacks" flag.
*/
private Builder()
{
super (EMultiReplyFeed.class,
ERequestMessage.class);
mCondition = NO_CONDITION;
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
@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)
.requireNotNull(mRequestCallback,
REQUEST_METHOD +
" not overridden or requestCallback not set")
.requireNotNull(mCancelCallback,
CANCEL_METHOD +
" not overridden or cancelRequestCallback not set"));
} // end of validate(Validator)
@Override
protected EReplyFeed createFeed(final EMessageKey key)
{
final EReplyFeed.Builder builder =
EReplyFeed.builder();
return (builder.target((EReplier) mTarget)
.location(mLocation)
.messageKey(key)
.scope(mScope)
.requestCallback(mRequestCallback)
.cancelRequestCallback(
mCancelCallback)
.build());
} // end of createFeed(EMessageKey)
@Override
protected EMultiReplyFeed buildImpl()
{
return (new EMultiReplyFeed(this));
} // end of buildImpl()
@Override
protected Builder self()
{
return (this);
} // end of self()
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// 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
} // end of class EMultiReplyFeed