net.sf.eBus.client.EMultiRequestFeed Maven / Gradle / Ivy
//
// Copyright 2017 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.Map;
import java.util.Objects;
import net.sf.eBus.client.ERequestFeed.ERequest;
import static net.sf.eBus.client.ERequestFeed.FEED_STATUS_METHOD;
import static net.sf.eBus.client.ERequestFeed.REPLY_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
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;
/**
* This feed acts as a proxy for handling multiple
* {@link ERequestFeed}s on behalf of a {@link ERequestor}
* client. A replier opens a multi-subject reply feed for a
* specified request message class and zero or more message
* subjects. These subjects may be specified as a list or a
* regular expression query. If a query is used, then the message
* class and subject query are used to search the message
* dictionary for all matching subjects. Matching subjects are
* used to create the initial subordinate {@code ERequestFeed}s.
*
* The multi-subject request feed coordinates subordinate request
* feeds so they are given the same configuration and are in the
* same state (open, subscribed, un-subscribed, closed).
*
*
* While the multi-subject feed is open, new subordinate request
* feeds may be {@link #addFeed(String) added to} or
* {@link #closeFeed(String) removed from} the multi-subject feed.
* Newly added subordinate feeds are configured and put into the
* same state as the existing subordinate feeds.
*
* Example use of EMultiRequestFeed
* 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;
import net.sf.eBus.util.regex.Pattern;
Step 1: Implement the ERequestor interface.
public class CatalogRequestor implements ERequestor {
// Select subjects matching this query pattern.
private final Pattern mQuery;
private final FeedScope mScope;
private final List<ERequestFeed.ERequest> mRequests;
private EMultiRequestFeed mFeed;
public CatalogRequestor(final Pattern query, final FeedScope scope) {
mQuery = query;
mScope = scope;
mRequests = new ArrayList<>();
}
@Override
public void startup() {
try {
Step 2: Open request feed. May override ERequestor interface methods.
mFeed = (EMultiRequestFeed.builder()).target(this)
.scope(mScope)
.messageClass(com.acme.CatalogOrder.class)
.query(mQuery)
.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 for given subject.
} else {
// Up. There is at least one replier for subject.
}
}
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 EMultiReplyFeed
*
* @author Charles W. Rapp
*/
public final class EMultiRequestFeed
extends EMultiFeed
implements IERequestFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Locals.
//
/**
* Contains the functional interface callback for feed
* status updates. If not explicitly set by client, then
* defaults to
* {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}.
* This callback is applied to all subordinate request feeds.
*/
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)}.
* This callback is applied to all subordinate request feeds.
*/
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 multi-request feed instance based on
* validated builder settings.
* @param builder contains multi-request feed settings.
*/
private EMultiRequestFeed(final Builder builder)
{
super (builder);
mStatusCallback = builder.mStatusCallback;
mReplyCallbacks = builder.mReplyCallbacks;
mCloseFlag = builder.mCloseFlag;
} // end of EMultiRequestFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Returns a newly minted subordinate request feed for the
* given key. The returned feed's configuration is the same
* as existing subordinate feeds.
* @param key create a request feed for this key.
* @return a subordinate request feed.
*/
@Override
protected ERequestFeed createFeed(final EMessageKey key)
{
final ERequestor requester =
(ERequestor) mEClient.target();
final ERequestFeed.Builder builder =
ERequestFeed.builder();
builder.target(requester)
.location(location())
.messageKey(key)
.scope(mScope)
.mayClose(mCloseFlag)
.statusCallback(mStatusCallback);
mReplyCallbacks.entrySet()
.stream()
.forEach(
e ->
builder.replyCallback(
e.getKey(), e.getValue()));
return (builder.build());
} // end of createFeed(EMessageKey)
/**
* Sets the status and reply callbacks as per the multi-subject
* configuration and subscribes the subordinate feeds.
* @param feed advertise this subordinate request feed.
*/
@Override
protected void putFeedInPlace(final ERequestFeed feed)
{
// Must set the callbacks (if any) before subscribing.
feed.replyCallbacks(mReplyCallbacks);
feed.subscribe();
} // end of putFeedInPlace(ERequestFeed)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
/**
* Returns a new {@code EMultiRequestFeed} 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 each subordinate {@link ERequestFeed}. If this
* feed is currently subscribed, then does nothing. The
* requestor client will receive a
* {@link ERequestor#feedStatus(EFeedState, IERequestFeed)}
* callback from each subordinate request feed.
* @throws IllegalStateException
* if this feed is closed or the client did not override
* not put in place the required callback methods.
*
* @see #unsubscribe()
* @see #close()
*/
@Override
public void subscribe()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
// If not already subscribed, then subscribe now.
if (!mInPlace)
{
sLogger.debug(
"{} multi-subject requestor {}: subscribing ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Subscribe each subordinate feed.
mFeeds.values()
.stream()
.map(
feed ->
{
feed.replyCallbacks(mReplyCallbacks);
return (feed);
})
.forEachOrdered(ERequestFeed::subscribe);
mInPlace = true;
// If this multi-subject feed is query based, then
// listen for subject updates.
if (mQuery != null)
{
ESubject.addListener(this);
}
}
} // end of subscribe()
/**
* Retracts this multi-subject request feed by un-subscribing
* each subordinate request feed. Does nothing if this feed
* is not currently subscribed.
* @throws IllegalStateException
* if this multi-subject request feed is closed.
*
* @see #subscribe()
* @see #close()
*/
@Override
public void unsubscribe()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
if (mInPlace)
{
sLogger.debug(
"{} multi-subject requestor {}: unsubscribing ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Unadvertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(ERequestFeed::unsubscribe);
// This feed is no longer subscribed.
mInPlace = false;
// Stop listening for subject updates.
ESubject.removeListener(this);
}
} // end of unsubscribe()
/**
* Posts a request message to all replier via the subordinate
* request feed matching the message's key.
* @param msg post this request message to the matching
* subordinate feed.
* @return the {@link ERequestFeed.ERequest} feed used to
* interact with the active request.
* @throws NullPointerException
* if {@code msg} is {@code null}.
* @throws IllegalArgumentException
* if {@code msg} message key does not reference a known
* subordinate reply feed.
* @throws IllegalStateException
* if this feed is inactive, not advertised, or there are no
* repliers available to respond to the request.
*/
public ERequest request(final ERequestMessage msg)
{
// Is the message null?
Objects.requireNonNull(msg, "msg is null");
// Is this feed still active?
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
// Is the subscription in place?
if (!mInPlace)
{
// No. Gotta do that first.
throw (
new IllegalStateException(
"feed not subscribed"));
}
// Does the message reference a known subordinate feed?
final String subject = (msg.key()).subject();
if (!mFeeds.containsKey(subject))
{
// No.
throw (
new IllegalArgumentException(
subject + " is an unknown feed"));
}
// Pass the message to the subordinate request and let
// it do the additional checks.
return ((mFeeds.get(subject)).doRequest(msg));
} // end of request(ERequestMessage)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EMultiRequestFeed.Builder} is the mechanism for
* creating an {@code EMultiRequestFeed} instance. A
* {@code Builder} instance is acquired from
* {@link EMultiRequestFeed#builder()}. The following example
* shows how to create an {@code EMultiRequestFeed} instance
* using a {@code Builder}. The code assumes that the target
* class implements {@code ERequestor} interface methods.
* @Overricde public void startup() {
final EMultiRequestFeed feed = (EMultiRequestFeed.builder()).target(this)
.messageClass(com.acme.CatalogOrder.class)
.scope(EFeed.FeedScope.REMOTE_ONLY)
.subjects(mSubjectsList) // msSubjectsList is List<String> subject list
// Call .statusCallback(lambda expression) and
// .replyCallback(lambda expression) to replace feedStatus, reply methods
.build();
}
*/
public static final class Builder
extends EMultiFeed.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.
//
/**
* Creates a new multi-request feed builder instance.
* "May close" flag set to {@code true}.
*/
private Builder()
{
super (EMultiRequestFeed.class,
ERequestMessage.class);
mCloseFlag = true;
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
@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)
.requireNotNull(mStatusCallback,
"statusCallback");
checkReplyCallbacks(problems);
return (problems);
} // end of validate(Validator)
@Override
protected ERequestFeed createFeed(final EMessageKey key)
{
final ERequestFeed.Builder builder =
ERequestFeed.builder();
builder.target((ERequestor) mTarget)
.location(mLocation)
.messageKey(key)
.scope(mScope)
.mayClose(mCloseFlag)
.statusCallback(mStatusCallback);
mReplyCallbacks.entrySet()
.stream()
.forEach(
e ->
builder.replyCallback(
e.getKey(), e.getValue()));
return (builder.build());
} // end of createFeed(EMessageKey)
@Override
protected EMultiRequestFeed buildImpl()
{
return (new EMultiRequestFeed(this));
} // end of buildImpl()
@Override
protected Builder self()
{
return (this);
} // end of self()
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
@Override
public Builder messageClass(final Class extends ERequestMessage> mc)
{
// Throws NullPointerException if mc is null.
super.messageClass(mc);
// Message class is set. Now fill in the reply
// callback map.
mReplyCallbacks =
ERequestFeed.createReplyCallbacks(
(MessageType) DataType.findType(mc));
return (this);
} // end of messageClass(final Class extends ERequestMessage> mc)
/**
* 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 #messageClass(java.lang.Class) message class}
* not set prior to setting reply callback.
*
* @see #messageClass(java.lang.Class)
* @see #replyCallback(ReplyCallback)
*/
public Builder replyCallback(final Class extends EReplyMessage> mc,
final ReplyCallback cb)
{
// Has the message class been set?
if (mMsgClass == null)
{
throw (
new IllegalStateException(
"messageClass must be set adding replyCallback"));
}
Objects.requireNonNull(mc, "mc is null");
if (!mReplyCallbacks.containsKey(mc))
{
throw (
new IllegalArgumentException(
mc.getSimpleName() + " is not a 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 #messageClass(java.lang.Class) message class}
* was not set prior to setting the reply callback.
*
* @see #replyCallback(Class, ReplyCallback)
*/
public Builder replyCallback(final ReplyCallback cb)
{
// Has the message class been set?
if (mMsgClass == null)
{
throw (
new IllegalStateException(
"messageClass 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
} // end of class EMultiRequestFeed