
net.sf.eBus.client.EMultiPublishFeed Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright 2017. Charles W. Rapp
// All Rights Reserved.
//
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.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.regex.Pattern;
/**
* This feed allows an {@link EPublisher} to open one feed for a
* given notification message class and multiple message subjects.
* It acts as a proxy between the publisher and the individual,
* subordinate feeds. The publisher interacts solely with the
* multi-key publisher. The publisher client opens, advertises,
* un-advertises, and closes the multi-key feed. In turn, the
* multi-key feed opens, advertises, un-advertises, and closes
* the subordinate {@link EPublishFeed}s in unison
* But the subordinate feeds issue
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* callbacks to the {@code EPublisher} registered with the
* multi-key feed. The multi-key feed does not callback to the
* publisher 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 either passing a
* message class and subject list to
* {@link #open(EPublisher, Class, List, EFeed.FeedScope) open}
* or a notification message class and regular express query to
* {@link #open(EPublisher, Class, Pattern, EFeed.FeedScope)}.
* The first limits the subordinate feeds to exactly those whose
* message key is listed. The second chooses message keys
* with the given message class and whose 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-key feed is open. When adding a new publish feed, the
* new feed is configured in the same was as existing feeds and
* put into the same state (open, advertised, etc.).
*
*
* @author Charles W. Rapp
*/
public final class EMultiPublishFeed
extends EMultiFeed
implements IEPublishFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Statics.
//
/**
* Lambda expression used to create a new subordinate publish
* feed.
*/
private static final SubordinateFeedFactory
sSubFactory =
(cl, key, sc, cond, loc) ->
EPublishFeed.open(cl, key, sc, loc, true);
/**
* Lambda expression used to create a new multi-key publish
* feed.
*/
private static final MultiFeedFactory
sMultiFactory =
(cl, mc, sc, cond, feeds) ->
new EMultiPublishFeed(cl, mc, sc, feeds);
//-----------------------------------------------------------
// Locals.
//
/**
* Tracks the publisher's ability to generate notification
* messages for this feed. {@link #mFeedState} tracks
* whether there are any subscribers to this feed.
*/
private EFeedState mPublishState;
/**
* Contains the functional interface callback for publish
* status updates. If not explicitly set by client, then
* defaults to
* {@link EPublisher#publishStatus(EFeedState, EPublishFeed)}.
* This callback is applied to each subordinate
* {@code EPublishFeed}.
*/
private FeedStatusCallback mStatusCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new multiple key publish feed for the given
* client, scope, and feeds. Note: {@code feeds} may be
* dynamic meaning that new {@code EPublishFeed} instances
* are added to the {@code feeds} list while this multi-key
* feed is active.
* @param client connect this client to the subordinate
* {@code EPublishFeed}s.
* @param mc all feeds apply to this notification message
* class.
* @param scope publish feed scope.
* @param feeds initial subordinate publish feed list.
*/
private EMultiPublishFeed(final EClient client,
final Class extends ENotificationMessage> mc,
final FeedScope scope,
final Map feeds)
{
super (client, mc, scope, null, feeds);
mPublishState = EFeedState.UNKNOWN;
mStatusCallback = null;
} // end of EMultiPublishFeed(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IEPublishFeed Interface Implementations.
//
/**
* Returns {@code true} if this multikey publish feed is both
* open and advertised; otherwise, returns {@code false}.
*
* Note: if {@code true} is returned, that does not
* mean that the publisher is clear to publish notification
* messages. The publisher should only post messages when
* {@link #isFeedUp(String)} returns {@code true}.
*
* @return {@code true} if this publish feed is open and
* advertised.
*
* @see #isActive()
* @see #isFeedUp(String)
* @see #feedState(String)
*/
@Override
public boolean isAdvertised()
{
return (mIsActive.get() && mInPlace);
} // end of isAdvertise()
/**
* Returns the publish state. The returned state is not to be
* confused with {@link #feedState(String)} which returns
* {@link EFeedState#UP} if there is any subscriber to this
* feed. The publish state specifies whether the publisher is
* capable of publishing messages for this feed.
*
* An up publish state does not mean that the publisher is
* clear to post notifications to the feed. See
* {@link #isFeedUp(String)} to determine if the publisher
* may post notifications to a specific subject.
*
* @return current publish state.
*
* @see #isFeedUp(String)
*/
@Override
public EFeedState publishState()
{
return (mPublishState);
} // end of publishState()
/**
* Returns {@code true} if the publisher is clear to publish
* a notification for the given feed and {@code false} if not
* clear. When {@code true} is returned, that means that this
* feed is 1) open, 2) advertised, 3) the publish state is up,
* and 4) there are subscribers listening to this
* notification feed.
*
* Returns {@code false} if {@code key} does not reference
* a known message feed.
*
* @param subject check the feed status for the feed
* referenced by this subject.
* @return {@code true} if the specified publisher feed is up
* and the publisher is free to publish notification
* messages for the specified subject.
* @throws NullPointerException
* if {@code subject} is {@code null}.
* @throws IllegalArgumentException
* if {@code subject} is empty.
* @throws IllegalStateException
* if this multi-key feed is closed.
*
* @see #isActive()
* @see #isAdvertised()
*/
@Override
public boolean isFeedUp(final String subject)
{
Objects.requireNonNull(subject, "subject is null");
if (subject.isEmpty())
{
throw (
new IllegalArgumentException(
"subject is an empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
final EPublishFeed feed = mFeeds.get(subject);
return (feed != null && feed.isFeedUp());
} // end of isFeedUp(String)
/**
* Puts the publish status callback in place. If {@code cb}
* is not {@code null}, publish status updates will be passed
* to {@code cb} rather than
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}.
* The reverse is true if {@code cb} is {@code null}. That
* is, a {@code null cb} means publish status updates are
* posted to the
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* override.
* @param cb the publish status update callback. May be
* {@code null}.
* @throws IllegalStateException
* if this feed is either closed or advertised.
*/
@Override
public void statusCallback(final FeedStatusCallback cb)
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
else if (mInPlace)
{
throw (
new IllegalStateException(
"advertisement in place"));
}
mStatusCallback = cb;
return;
} // end of statusCallback(FeedStatusCallback<>)
/**
* Advertises each subordinate {@link EPublishFeed}. If this
* feed is currently advertised, then does nothing. The
* publisher client will receive a
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* callback for each subordinate publish feed.
*
* The publisher may publish messages to this feed only
* after:
*
*
* -
* eBus calls
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* with an {@link EFeedState#UP up} feed state.
*
* -
* the publisher client calls
* {@link #updateFeedState(EFeedState)} with an
* {@code EFeedState.UP up} publish state. Note this
* feed state is applied to all subordinate feeds. It is
* not necessary for the publisher to call set the feed
* state for individual feeds.
*
*
*
* These two steps may occur in any order. Both states must
* be up before {@link #publish(ENotificationMessage)} may
* be called.
*
* @throws IllegalStateException
* if this feed is closed or the client did not override
* {@link EPublisher} methods nor put the required callback
* in place.
*
* @see #unadvertise()
* @see #updateFeedState(EFeedState)
* @see #close()
*/
@Override
public void advertise()
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (!mInPlace)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s multi-key publisher %d: advertising (%s).",
mEClient.location(),
mEClient.clientId(),
mScope));
}
// Advertise each subordinate feed.
mFeeds.values()
.stream()
.map(
feed ->
{
feed.statusCallback(mStatusCallback);
return feed;
})
.forEachOrdered(EPublishFeed::advertise);
// This feed is now advertised.
mInPlace = true;
}
return;
} // end of advertise()
/**
* Retracts this multi-key publisher feed by un-advertising
* each subordinate publish feed. Does nothing if this
* feed is not currently advertised.
* @throws IllegalStateException
* if this multi-key publisher feed is closed.
*
* @see #advertise()
* @see #close()
*/
@Override
public void unadvertise()
{
if (!mIsActive.get())
{
throw (
new IllegalStateException("feed is inactive"));
}
if (mInPlace)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s multi-key publisher %d: unadvertising (%s).",
mEClient.location(),
mEClient.clientId(),
mScope));
}
// Unadvertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(EPublishFeed::unadvertise);
// This feed is no longer advertised.
mPublishState = EFeedState.UNKNOWN;
mInPlace = false;
}
return ;
} // end of unadvertise()
/**
* Updates the publish 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 publish feed states are updated
* as well.
*
* The publish 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 publish 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 != mPublishState)
{
// Yes. Apply the update.
mPublishState = update;
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s multi-key publisher %d: setting feed state to %s (%s).",
mEClient.location(),
mEClient.clientId(),
update,
mScope));
}
// Update each subordinate publish feed.
mFeeds.values()
.forEach(feed -> feed.updateFeedState(update));
}
return;
} // end of updateFeedState(EFeedState)
/**
* Posts a notification message to all subscribers via the
* subordinate publish feed which matches the message's key.
* @param msg post this message to the matching subordinate
* feed.
* @throws NullPointerException
* if {@code msg} is {@code null}.
* @throws IllegalArgumentException
* if {@code msg} message key does not reference a known
* subordinate publish feed.
* @throws IllegalStateException
* if this feed is inactive, not advertised, the publisher
* has not declared the feed to be up, or there are no
* subscribers listening to the subordinate feed.
*/
@Override
public void publish(final ENotificationMessage 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 advertisement in place?
if (!mInPlace)
{
// No. Gotta do that first.
throw (
new IllegalStateException(
"feed not advertised"));
}
// Is the publisher state up?
if (mPublishState != EFeedState.UP)
{
// No. Gotta do that second.
throw (
new IllegalStateException(
"publish state is down"));
}
// 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"));
}
// So far, so good. Pass this message to the subordinate
// feed and let it do its thing.
(mFeeds.get(subject)).doPublish(msg);
return;
} // end of publish(ENotificationMessage)
//
// end of IEPublishFeed Interface Implementations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Returns a newly minted subordinate publish feed for the
* given key. The returned feed's configuration is the same
* as existing subordinate feeds.
* @param key create feed for this key.
* @return a subordinate publish feed.
*/
@Override
protected EPublishFeed createFeed(final EMessageKey key)
{
final EPublisher publisher =
(EPublisher) mEClient.target();
return (EPublishFeed.open(publisher,
key,
mScope,
ClientLocation.LOCAL,
true));
} // end of createFeed(EMessageKey)
/**
* Sets the feed status callback, advertises {@code feed},
* and updates the publish feed state. The publish feed state
* is taken from the most recent
* {@link #updateFeedState(EFeedState)} setting. If the
* publish feed state has never been updated, then the state
* is {@link EFeedState#UNKNOWN}.
* @param feed advertise this feed.
*
* @see #updateFeedState(EFeedState)
*/
@Override
protected void putFeedInPlace(final EPublishFeed feed)
{
feed.statusCallback(mStatusCallback);
feed.advertise();
feed.updateFeedState(mPublishState);
return;
} // end of putFeedInPlace(EPublishFeed)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
/**
* Returns an open publish feed for multiple notification
* message keys. Once opened, the caller can (optionally) set
* the status callback and advertise the feed just like
* {@link EPublishFeed}.
*
* Note: {@code client} receives callbacks
* for each subordinate {@code EPublishFeed} as if it opened
* those feeds directly. If {@code subjects} is large, then
* {@code client} must be prepared for a large number of
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* callbacks.
*
*
* {@code subjects} may be a non-{@code null}, empty list
* resulting in no initial subordinate publish feeds opened.
* This allows the publisher to start with an empty
* multi-key publisher feed, {@link #addFeed(String) adding}
* subordinate feeds later.
*
* @param client application object publishing the
* notification message class and subjects.
* @param mc notification message class. All feeds apply to
* this message class.
* @param subjects list of notification 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 publisher feed for the given
* application object, notification 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(EPublisher, Class, Pattern, EFeed.FeedScope)
* @see #statusCallback(FeedStatusCallback)
* @see #advertise()
* @see #close()
* @see #addFeed(String)
* @see #closeFeed(String)
*/
public static EMultiPublishFeed open(final EPublisher client,
final Class extends ENotificationMessage> mc,
final List subjects,
final FeedScope scope)
{
return (openList(client,
mc,
subjects,
scope,
null, // no condition for publish feeds.
sSubFactory,
sMultiFactory));
} // end of open(EPublisher, List<>, FeedScope)
/**
* Returns an open publish feed for a notification message
* class and multiple subjects. Once opened, the caller can
* (optionally) set the status callback and advertise the
* feed just like {@link EPublishFeed}.
*
* The subordinate publish feeds are selected based on the
* given notification message class and the regular
* expression pattern. If message class and {@code query} do
* not match any entries in the message key dictionary, then
* the returned multi-key publish feed will have no initial
* subordinate feeds. If that is the case, new publish feeds
* may be dynamically
* {@link EMultiFeed#addFeed(String) added} while
* the multi-key feed is open.
*
*
* Note: {@code client} receives callbacks
* for each subordinate {@code EPublishFeed} as if it opened
* those feeds directly. If {@code mc} and {@code query}
* matches a large number of notification message keys, then
* {@code client} must be prepared for a large number of
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* callbacks.
*
* @param client the application object publishing the
* notification message class and subjects.
* @param mc the message key query is for this notification
* message class only.
* @param query message key subject query.
* @param scope whether the feed supports local feeds, remote
* feeds, or both.
* @return a new multiple key publisher feed for the given
* application object and notification message keys matching
* the query.
* @throws NullPointerException
* if any of the arguments are {@code null}.
* @throws IllegalArgumentException
* if any of the arguments is invalid.
*
* @see #open(EPublisher, Class, List, EFeed.FeedScope)
* @see #statusCallback(FeedStatusCallback)
* @see #advertise()
* @see #close()
* @see #addFeed(String)
* @see #closeFeed(String)
*/
public static EMultiPublishFeed open(final EPublisher client,
final Class extends ENotificationMessage> mc,
final Pattern query,
final FeedScope scope)
{
return (openQuery(client,
mc,
query,
scope,
null,
sSubFactory,
sMultiFactory));
} // end of open(EPublisher, Pattern, boolean, FeedScope)
} // end of class EMultiPublishFeed