
net.sf.eBus.client.EMultiPublishFeed Maven / Gradle / Ivy
//
// Copyright 2017, 2020, 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.EPublishFeed.PUB_STATUS_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.Validator;
/**
* 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-subject publisher. The publisher client opens,
* advertises, un-advertises, and closes the multi-subject feed.
* In turn, the multi-subject 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-subject feed. The multi-subject 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.
*
* Subordinate feeds are selected by passing a notification
* message class to
* {@link EMultiFeed.Builder#messageClass(java.lang.Class) EMultiPublishFeed.Builder.messageKey}
* and either a subject list to
* {@link EMultiFeed.Builder#subjects(java.util.List) EMultiPublishFeed.Builder.subjects}
* or a regular express query to
* {@link EMultiFeed.Builder#query(net.sf.eBus.util.regex.Pattern) EMultiPublishFeed.Builder.query}.
* 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-subject 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.).
*
* Example use of EMultiPublishFeed
* import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.EMultiPublishFeed;
import net.sf.eBus.client.EPublisher;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.regex.Pattern;
// Step 1: Implement EPublisher interface.
public class CatalogPublisher implements EPublisher {
// Select subjects matching this query pattern.
private final Pattern mQuery;
// Published message scope.
private final FeedScope mScope;
// Advertise and publish on this feed.
private EMultiPublishFeed mFeed;
public CatalogPublisher(final Pattern query, final FeedScope scope) {
mQuery = query;
mScope = scope;
}
@Override
public void startup() {
try {
// Step 2: Open multi-subject publish feed. Place EPublisher interface method overrides here.
// This publisher overrides EPublisher interface method.
mFeed = (EMultiPublishFeed.builder()).target(this)
.scope(mScope)
.messageClass(CatalogUpdate.class)
.query(mQuery)
.build();
// Step 3: Advertise this publisher to eBus.
mFeed.advertise();
// Inform the world that this publisher's feed state is up.
mFeed.updateFeedState(EFeedState.UP);
} catch (IllegalArgumentException argex) {
// Advertisement failed. Place recovery code here.
}
}
// Step 4: Handle publish status update.
@Override
public void publishStatus(final EFeedState feedState, final IEPublishFeed feed) {
final String subject = (feed.key()).subject();
// Are we starting a feed?
if (feedState == EFeedState.UP) {
// Yes. Start publishing notifications on the feed.
startPublishing(subject);
} else {
// We are stopping the feed.
stopPublishing(subject);
}
}
public void updateProduct(final String subject, final String productName, final Money price, final int stockQty) {
if (mFeed != null && mFeed.isFeedUp(subject)) {
// Step 5: Start publishing notifications.
mFeed.publish((CatalogUpdate.builder()).subject(subject)
.timestamp(Instant.now())
.productName(productName)
.price(price)
.inStockQuantity(stockQty)
.build());
}
}
// Retract the notification feed.
@Override
public void shutdown() {
Step 6: On shutdown either unadvertise or close publish feed.
if (mFeed != null) {
// unadvertise() unnecessary since close() retracts an in-place advertisement.
mFeed.close();
mFeed = null;
}
}
// Starts the notification feed when the feed state is up.
// Return EFeedState.UP if the notification is successfully started;
// EFeedState.DOWN if the feed fails to start.
private EFeedState startPublishing(final String subject) {
Application-specific code not shown.
}
// Stops the notification feed if up.
private void stopPublishing(final String subject) {
Application-specific code not shown.
}
}
*
* @see EPublisher
* @see EMultiSubscribeFeed
*
* @author Charles W. Rapp
*/
public final class EMultiPublishFeed
extends EMultiFeed
implements IEPublishFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// 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 final FeedStatusCallback mStatusCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
private EMultiPublishFeed(final Builder builder)
{
super (builder);
mPublishState = EFeedState.UNKNOWN;
mStatusCallback = builder.mStatusCallback;
} // end of EMultiPublishFeed(Builder)
//
// 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-subject 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-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
final EPublishFeed feed = mFeeds.get(subject);
return (feed != null && feed.isFeedUp());
} // end of isFeedUp(String)
/**
* 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)
{
sLogger.debug(
"{} multi-subject publisher {}: advertising ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Advertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(EPublishFeed::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 publisher feed by
* un-advertising each subordinate publish feed. Does nothing
* if this feed is not currently advertised.
* @throws IllegalStateException
* if this multi-subject publisher 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 publisher {}: unadvertising ({}).",
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;
// Stop listening for subject updates.
ESubject.removeListener(this);
}
} // end of unadvertise()
/**
* Updates the publish feed state for this multi-feed and all
* message keys to the given value. If {@code update} equals
* the currently stored multi-feed 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;
sLogger.debug(
"{} multi-subject publisher {}: 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)
/**
* 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);
} // 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 EPublishFeed.Builder builder =
EPublishFeed.builder();
return (builder.target((EPublisher) mEClient.target())
.location(location())
.messageKey(key)
.scope(mScope)
.statusCallback(mStatusCallback)
.build());
} // 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.advertise();
feed.updateFeedState(mPublishState);
} // end of putFeedInPlace(EPublishFeed)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
/**
* Returns a new multi-publish feed builder. It is
* recommended that a new {@code Builder} instance be used
* for each {@code EMultiPublishFeed} creation.
* @return new multi-publish feed builder instance.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
/**
* Updates publish feed state for a single subject in the
* multi-feed publisher. The overall multi-feed publish feed
* state is left unchanged.
*
* 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 subject subject currently in multi-feed.
* @param update new publish feed state.
* @throws NullPointerException
* if {@code subject} or {@code update} is {@code null}.
* @throws IllegalStateException
* if this feed was closed or is not advertised.
* @throws IllegalArgumentException
* if {@code subject} is not part of the multi-feed.
*/
public void updateFeedState(final String subject,
final EFeedState update)
{
Objects.requireNonNull(subject, "subject is null");
Objects.requireNonNull(update, "update is null");
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
else if (!mInPlace)
{
throw (
new IllegalStateException(FEED_NOT_ADVERTISED));
}
else if (!mFeeds.containsKey(subject))
{
throw (
new IllegalArgumentException(
subject + " is not part of this multi-feed"));
}
else
{
(mFeeds.get(subject)).updateFeedState(update);
}
} // end of updateFeedState(String, EFeedState)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EMultiPublishFeed.Builder} is the mechanism for
* creating an {@code EMultiPublishFeed} instance. A
* {@code Builder} instance is acquired from
* {@link EMultiPublishFeed#builder()}. The following
* example shows how to create an {@code EMultiPublishFeed}
* instance using a {@code Builder}. The code assumes that
* the target class implements {@code EPublisher} interface
* {@code feedStatus} method.
* @Overricde public void startup() {
final EMultiPublishFeed feed = (EMultiPublishFeed.builder()).target(this)
.messageClass(CatalogUpdate.class)
.scope(EFeed.FeedScope.LOCAL_AND_REMOTE)
// Call .statusCallback(lambda expression) to replace feedStatus method
.build();
...
}
*/
public static final class Builder
extends EMultiFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Contains the functional interface callback for publish
* status updates. If not explicitly set by client, then
* defaults to
* {@link EPublisher#publishStatus(EFeedState, EPublishFeed)}.
*/
private FeedStatusCallback mStatusCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private Builder()
{
super (EMultiPublishFeed.class,
ENotificationMessage.class);
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
@Override
protected Validator validate(final Validator problems)
{
// Was a publish status callback put in place?
// Did the publisher override EPublisher methods?
if (mStatusCallback == null &&
mTarget != null &&
isOverridden(PUB_STATUS_METHOD,
EFeedState.class,
IEPublishFeed.class))
{
// Create a callback back to
// EPublisher.publishStatus() method.
mStatusCallback =
((EPublisher) mTarget)::publishStatus;
}
return (super.validate(problems)
.requireNotNull(
mStatusCallback,
PUB_STATUS_METHOD +
" not overridden and statusCallback not set"));
} // end of validate(Validator)
@Override
protected EPublishFeed createFeed(final EMessageKey key)
{
final EPublishFeed.Builder builder =
EPublishFeed.builder();
return (builder.target((EPublisher) mTarget)
.location(mLocation)
.messageKey(key)
.scope(mScope)
.statusCallback(mStatusCallback)
.build());
} // end of createFeed(EMessageKey)
@Override
protected EMultiPublishFeed buildImpl()
{
return (new EMultiPublishFeed(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.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* 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},
* then updates are posted to the
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* override.
*
* The following example shows how to use this method:
*
* statusCallback(
(fs, f) →
{
if (fs == EFeedState.DOWN) {
// Clean up in-progress work.
}
}
* @param cb publish 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<>)
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class Builder
} // end of class EMultiPublishFeed