
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.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.ERequestFeed.ERequest;
import net.sf.eBus.messages.EMessage;
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.regex.Pattern;
/**
* This feed acts as a proxy for handling multiple
* {@link ERequestFeed}s on behalf of a {@link ERequestor}
* client. A replier opens a multi-key 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 key dictionary for all
* matching subjects. The matching subjects are used to create
* the initial subordinate {@code ERequestFeed}s.
*
* The multi-key request feed coordinates the subordinate request
* feeds so they are given the same configuration and are in the
* same state (open, subscribed, un-subscribed, closed).
*
*
* While the multi-key feed is open, new subordinate request
* feeds may be {@link #addFeed(String) added to} or
* {@link #closeFeed(String) removed from} the multi-key feed.
* Newly added subordinate feeds are configured and put into the
* same state as the existing subordinate feeds.
*
*
* @author Charles W. Rapp
*/
public final class EMultiRequestFeed
extends EMultiFeed
implements IERequestFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Statics.
//
/**
* Lambda expression used to create a new subordinate reply
* feed.
*/
private static final SubordinateFeedFactory
sSubFactory =
(cl, key, sc, cond, loc) ->
ERequestFeed.open(cl, key, sc, loc, true);
/**
* Lambda expression used to create a new multi-key reply
* feed.
*/
private static final MultiFeedFactory
sMultiFactory =
(cl, mc, sc, cond, feeds) ->
{
final MessageType mt =
(MessageType) DataType.findType(mc);
final Map, ReplyCallback> cbs =
ERequestFeed.createReplyCallbacks(mt);
return (new EMultiRequestFeed(cl,
mc,
sc,
feeds,
cbs));
};
//-----------------------------------------------------------
// 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 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;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new instance of EMultiRequestFeed.
* @param cbs reply callback map with all message keys
* set to {@code null} callbacks.
*/
private EMultiRequestFeed(final EClient client,
final Class extends ERequestMessage> mc,
final FeedScope scope,
final Map feeds,
final Map, ReplyCallback> cbs)
{
super (client, mc, scope, null, feeds);
mStatusCallback = null;
mReplyCallbacks = cbs;
} // end of EMultiRequestFeed(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IERequestFeed Interface Implementations.
//
/**
* 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.
* @param cb feed status update callback. May be
* {@code null}.
* @throws IllegalStateException
* if this feed is either closed or subscribed.
*/
@Override
public void statusCallback(final FeedStatusCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"subscription in place"));
}
mStatusCallback = cb;
} // end of statusCallback(FeedStatusCallback<>)
/**
* Puts the reply message callback in place
* for all reply types. If {@code cb} is
* not {@code null}, replies will be passed to {@code cb}
* rather than
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}.
* A {@code null cb} results in replies posted to the
* {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)}
* override.
*
* Note that this method call overrides all previous calls
* to {@link #replyCallback(Class, ReplyCallback)}. If
* the goal is to use a generic callback for all replies
* except one specific message, then use this method to put
* the generic callback in place first and then use
* {@code replyCallback(EMessageKey, ReplyCallback)}.
*
* @param cb reply message callback. May be {@code null}.
* @throws IllegalStateException
* if this feed is either closed or subscribed.
*
* @see #replyCallback(Class, ReplyCallback)
*/
@Override
public void replyCallback(final ReplyCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
throw (
new IllegalStateException(
"subscription in place"));
}
mReplyCallbacks.entrySet()
.forEach(entry -> entry.setValue(cb));
} // end of replyCallback(ReplyCallback)
/**
* Sets the callback for a specific reply message class. If
* {@code cb} is not {@code null}, replies will be passed to
* {@code cb} rather than
* {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}.
* A {@code cb} results in replies posted to the
* {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)}
* override.
*
* If the goal is to set a single callback method for all
* reply message types, then use
* {@link #replyCallback(ReplyCallback)}. Note that method
* overrides all previous set reply callbacks.
*
* @param mc the reply message class.
* @param cb callback for the reply message.
* @throws NullPointerException
* if {@code mc} is {@code null}.
* @throws IllegalArgumentException
* if {@code mc} is not a reply for this request.
* @throws IllegalStateException
* if this feed is either closed or subscribed.
*/
@Override
public void replyCallback(final Class extends EReplyMessage> mc,
final ReplyCallback cb)
{
Objects.requireNonNull(mc, "mc is null");
if (!mReplyCallbacks.containsKey(mc))
{
throw (
new IllegalArgumentException(
mc.getSimpleName() +
" is not a " +
mMsgClass.getSimpleName() +
" 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)
//
// end of IERequestFeed Interface Implementations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// 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 requestor =
(ERequestor) mEClient.target();
return (ERequestFeed.open(requestor,
key,
mScope,
EClient.ClientLocation.LOCAL,
true));
} // end of createFeed(EMessageKey)
/**
* Sets the status and reply callbacks as per the multi-key
* 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.statusCallback(mStatusCallback);
feed.replyCallbacks(mReplyCallbacks);
feed.subscribe();
} // end of putFeedInPlace(ERequestFeed)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
/**
* Returns an open multi-key feed for the given request
* message class and message subjects list. Once opened,
* the caller can (optionally) set the feed status and
* reply callbacks and subject the feed just like
* {@link ERequestFeed}.
*
* It is the subordinate request feeds which call back to
* {@code client}, not the opened multi-key feed. So if
* {@code subjects} contains 1,000 subjects, then
* {@code client} receives status callbacks from 1,000
* subordinate request feeds.
*
*
* {@code subjects} may be a non-{@code null}, empty list
* resulting in no initial subordinate request feeds opened.
* This allows the requestor to start with an empty
* multi-key request feed, {@link #addFeed(String) adding}
* subordinate feeds later.
*
* @param client the eBus requestor opening this feed.
* @param mc request message class. All feeds apply to
* this message class.
* @param subjects list of request message subjects. May not
* contain {@code null} or empty strings but this list may be
* empty.
* @param scope whether the feed supports local feeds,
* remote feeds, or both.
* @return a new multiple key requestor feed for the given
* application object, request message class,and subjects.
* @throws NullPointerException
* if any of the arguments is {@code null}.
* @throws IllegalArgumentException
* if {@code subjects} contains an empty string.
*
* @see #open(ERequestor, Class, Pattern, EFeed.FeedScope)
* @see #subscribe()
* @see #close()
* @see #addFeed(String)
* @see #closeFeed(String)
*/
public static EMultiRequestFeed open(final ERequestor client,
final Class extends ERequestMessage> mc,
final List subjects,
final FeedScope scope)
{
return (openList(client,
mc,
subjects,
scope,
null,
ClientLocation.LOCAL,
sSubFactory,
sMultiFactory));
} // end of open(ERequestor, Class, List<>, FeedScope)
/**
* Returns an open multi-key request feed for a given
* request message class and multiple message subjects.
* Once opened, the caller can (optionally) set the status
* and reply callbacks and subscribe the feed just like
* {@link ERequestFeed}.
*
* The subordinate request feeds are selected based on the
* given request message class and the regular expression
* query. The multi-key request feed is opened whether the
* message key dictionary entries match the message class and
* subject query or not. In either case, the application may
* {@link #addFeed(String) add} more subordinate feeds to the
* returned multi-key feed.
*
*
* Note: {@code client} receives callbacks
* from subordinate {@code ERequestFeed} feeds, not the
* multi-key feed. If {@code mc} and {@code query} match
* 1,000 request message keys, then {@code client} will
* receive feed status and notify callbacks from those 1,000
* subordinate feeds.
*
* @param client application object subscribing to the
* request message class and matching subjects.
* @param mc the message key query is for this request
* message class only.
* @param query message key subject query.
* @param scope whether the feed supports local feeds, remote
* feeds, or both.
* @return a new multi-key request feed for the given
* application object and request message keys matching the
* request message class and query.
*
* @see #open(ERequestor, Class, List, EFeed.FeedScope)
* @see #subscribe()
* @see #close()
* @see #addFeed(String)
* @see #closeFeed(String)
*/
public static EMultiRequestFeed open(final ERequestor client,
final Class extends ERequestMessage> mc,
final Pattern query,
final FeedScope scope)
{
return (openQuery(client,
mc,
query,
scope,
null,
ClientLocation.LOCAL,
sSubFactory,
sMultiFactory));
} // end of open(ERequestor, Class, Pattern, FeedScope)
/**
* 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)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s multi-key requestor %d: subscribing (%s).",
mEClient.location(),
mEClient.clientId(),
mScope));
}
// Subscribe each subordinate feed.
mFeeds.values()
.stream()
.map(
feed ->
{
feed.statusCallback(mStatusCallback);
feed.replyCallbacks(mReplyCallbacks);
return feed;
})
.forEachOrdered(ERequestFeed::subscribe);
mInPlace = true;
}
} // end of subscribe()
/**
* Retracts this multi-key request feed by un-subscribing
* each subordinate request feed. Does nothing if this feed
* is not currently subscribed.
* @throws IllegalStateException
* if this multi-key request feed is closed.
*
* @see #subscribe()
* @see #close()
*/
@Override
public void unsubscribe()
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s multi-key requestor %d: unsubscribing (%s).",
mEClient.location(),
mEClient.clientId(),
mScope));
}
// Unadvertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(ERequestFeed::unsubscribe);
// This feed is no longer subscribed.
mInPlace = false;
}
} // 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)
/**
* Returns an open multi-key feed for the given request
* message class and message subjects list. Once opened,
* the caller can (optionally) set the feed status and
* reply callbacks and subject the feed just like
* {@link ERequestFeed}.
*
* It is the subordinate request feeds which call back to
* {@code client}, not the opened multi-key feed. So if
* {@code subjects} contains 1,000 subjects, then
* {@code client} receives status callbacks from 1,000
* subordinate request feeds.
*
*
* {@code subjects} may be a non-{@code null}, empty list
* resulting in no initial subordinate request feeds opened.
* This allows the requestor to start with an empty
* multi-key request feed, {@link #addFeed(String) adding}
* subordinate feeds later.
*
* @param client the eBus requestor opening this feed.
* @param mc request message class. All feeds apply to
* this message class.
* @param subjects list of request message subjects. May not
* contain {@code null} or empty strings but this list may be
* empty.
* @param scope whether the feed supports local feeds,
* remote feeds, or both.
* @param l {@code client} location.
* @return a new multiple key requestor feed for the given
* application object, request message class,and subjects.
* @throws NullPointerException
* if any of the arguments is {@code null}.
* @throws IllegalArgumentException
* if {@code subjects} contains an empty string.
*
* @see #open(ERequestor, Class, Pattern, EFeed.FeedScope)
* @see #subscribe()
* @see #close()
* @see #addFeed(String)
* @see #closeFeed(String)
*/
/* package */ static EMultiRequestFeed open(final ERequestor client,
final Class extends ERequestMessage> mc,
final List subjects,
final FeedScope scope,
final ClientLocation l)
{
return (openList(client,
mc,
subjects,
scope,
null,
l,
sSubFactory,
sMultiFactory));
} // end of open(...)
/**
* Returns an open multi-key request feed for a given
* request message class and multiple message subjects.
* Once opened, the caller can (optionally) set the status
* and reply callbacks and subscribe the feed just like
* {@link ERequestFeed}.
*
* The subordinate request feeds are selected based on the
* given request message class and the regular expression
* query. The multi-key request feed is opened whether the
* message key dictionary entries match the message class and
* subject query or not. In either case, the application may
* {@link #addFeed(String) add} more subordinate feeds to the
* returned multi-key feed.
*
*
* Note: {@code client} receives callbacks
* from subordinate {@code ERequestFeed} feeds, not the
* multi-key feed. If {@code mc} and {@code query} match
* 1,000 request message keys, then {@code client} will
* receive feed status and notify callbacks from those 1,000
* subordinate feeds.
*
* @param client application object subscribing to the
* request message class and matching subjects.
* @param mc the message key query is for this request
* message class only.
* @param query message key subject query.
* @param scope whether the feed supports local feeds, remote
* feeds, or both.
* @param l {@code client} location.
* @return a new multi-key request feed for the given
* application object and request message keys matching the
* request message class and query.
*
* @see #open(ERequestor, Class, List, EFeed.FeedScope)
* @see #subscribe()
* @see #close()
* @see #addFeed(String)
* @see #closeFeed(String)
*/
/* package */ static EMultiRequestFeed open(final ERequestor client,
final Class extends ERequestMessage> mc,
final Pattern query,
final FeedScope scope,
final ClientLocation l)
{
return (openQuery(client,
mc,
query,
scope,
null,
l,
sSubFactory,
sMultiFactory));
} // end of open(...)
} // end of class EMultiRequestFeed