net.sf.eBus.client.EMultiSubscribeFeed Maven / Gradle / Ivy
The newest version!
//
// 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 static net.sf.eBus.client.EFeed.NOTIFY_METHOD;
import static net.sf.eBus.client.EFeed.NO_CONDITION;
import static net.sf.eBus.client.ESubscribeFeed.FEED_STATUS_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.Validator;
/**
* This feeds acts as a proxy for handling multiple
* {@link ESubscribeFeed}s on behalf of a {@link ESubscriber}
* client. A subscriber opens a multi-subject subscribe feed for
* a specified notification message class and zero or more
* message subjects. 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 to search the
* message key dictionary for all matching subjects. The matching
* subjects are used to create the initial subordinate
* {@code ESubjectFeed}s.
*
* The multi-subject subscribe feed coordinates the subordinate
* subscribe feeds to 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
* subscribe 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 EMultiSubscribeFeed
* import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.ESubscribeFeed;
import net.sf.eBus.client.ESubscriber;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.regex.Pattern;
Step 1: Implement the ESubscriber interface.
public class CatalogSubscriber implements ESubscriber {
// Select subjects matching this query pattern.
private final Pattern mQuery;
// Subscribe to this feed scope.
private final FeedScope mScope;
// Store the feed here so it can be used to unsubscribe.
private EMultiSubscribeFeed mFeed;
public CatalogSubscriber(final Pattern query, final FeedScope scope) {
mQuery = query;
mScope = scope;
}
@Override public void startup() {
try {
Step 2: Open the ESubscribe feed.
// This subscriber has no associated ECondition and uses ESubscriber interface method overrides.
mFeed = (EMultiSubscribeFeed.builder()).target(this)
.scope(mScope)
.messageClass(CatalogUpdate.class)
.query(mQuery)
.build();
Step 3: Subscribe to the feed.
mFeed.subscribe();
} catch(IllegalArgumentException argex) {
// Feed open failed. Place recovery code here.
}
}
Step 4: Wait for EFeedState.UP feed status.
@Override public void feedStatus(final EFeedState feedState, final IESubscribeFeed feed) {
// What is the feed state?
if (feedState == EFeedState.DOWN) {
// Down. There are no publishers. Expect no notifications until a
// publisher is found. Put error recovery code here.
} else {
// Up. There is at least one publisher. Expect to receive notifications.
}
}
Step 5: Wait for notifications to arrive.
@Override public void notify(final ENotificationMessage msg, final IESubscribeFeed feed) {
// Notification handling code here.
}
@Override public void shutdown() {
Step 6: When subscriber is shutting down, retract subscription feed.
// mFeed.unsubscribe() is not necessary since close() will unsubscribe.
if (mFeed != null) {
mFeed.close();
mFeed = null;
}
}
}
*
* @see ESubscriber
* @see EMultiPublishFeed
*
* @author Charles W. Rapp
*/
public final class EMultiSubscribeFeed
extends EMultiFeed
implements IESubscribeFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Locals.
//
/**
* Use this condition to check if received message should be
* forwarded to client. This condition is applied to all
* subordinate feeds which use conditions. This data member
* is {@code null} if the subordinate feed does not support
* message condition.
*/
protected final ECondition mCondition;
/**
* Feed status callback. If not explicitly set by client,
* then defaults to
* {@link ESubscriber#feedStatus(EFeedState, ESubscribeFeed)}.
* Applied to all subordinate subscribe feeds.
*/
private final FeedStatusCallback mStatusCallback;
/**
* Notification message callback. If not explicity set by
* client, then defaults to
* {@link ESubscriber#notify(ENotificationMessage, ESubscribeFeed)}.
* Applied to all subordinate subscribe feeds.
*/
private final NotifyCallback extends ENotificationMessage> mNotifyCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new multi-subscribe feed instance based on the
* validated builder settings.
* @param builder contains validated feed configuration.
*/
private EMultiSubscribeFeed(final Builder builder)
{
super (builder);
mCondition = builder.mCondition;
mStatusCallback = builder.mStatusCallback;
mNotifyCallback = builder.mNotifyCallback;
} // end of EMultiSubscribeFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IESubscribeFeed Interface Implementations.
//
/**
* Subscribes each subordinate {@link ESubscribeFeed}. If
* this feed is currently subscribed, then does nothing. The
* subscriber client will receive a
* {@link ESubscriber#feedStatus(EFeedState, IESubscribeFeed)}
* callback from each subordinate subscribe feed.
* @throws IllegalStateException
* if this feed is closed or the client did not override
* {@link ESubscriber} methods nor put the required callback
* in place.
*
* @see #unsubscribe()
* @see #close()
*/
@Override
public void subscribe()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
if (!mInPlace)
{
sLogger.debug(
"{} multi-subject subscriber {}: subscribing ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Subscribe each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(ESubscribeFeed::subscribe);
// 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 subscribe()
/**
* Retracts this multi-subject subscribe feed by un-subscribing
* each subordinate subscribe feed. Does nothing if this
* feed is not currently subscribed.
* @throws IllegalStateException
* if this multi-subject subscribe 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 subscriber {}: unsubscribing ({}).",
mEClient.location(),
mEClient.clientId(),
mScope);
// Unadvertise each subordinate feed.
mFeeds.values()
.stream()
.forEachOrdered(ESubscribeFeed::unsubscribe);
// This feed is no longer subscribed.
mInPlace = false;
// Stop listening for subject updates.
ESubject.removeListener(this);
}
} // end of unsubscribe()
//
// end of IESubscribeFeed Interface Implementations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Implementations.
//
/**
* Returns a newly minted subordinate subscribe feed for the
* given key.
* @param key create feed for this key.
* @return a subordinate subscribe feed.
*/
@Override
protected ESubscribeFeed createFeed(final EMessageKey key)
{
final ESubscribeFeed.Builder builder =
ESubscribeFeed.builder();
return (builder.target((ESubscriber) mEClient.target())
.location(location())
.messageKey(key)
.scope(mScope)
.statusCallback(mStatusCallback)
.notifyCallback(mNotifyCallback)
.build());
} // end of createFeed(EMessageKey)
/**
* Sets the callbacks and subscribes the {@code feed}.
* @param feed subscribe this feed.
*/
@Override
protected void putFeedInPlace(final ESubscribeFeed feed)
{
feed.subscribe();
} // end of putFeedInPlace(ESubscribeFeed)
//
// end of Abstract Method Implementations.
//-----------------------------------------------------------
/**
* Returns a new multi-subscribe feed builder. It is
* recommended that a new {@code Builder} instance be used
* for each {@code EMultiSubscribeFeed} creation.
* @return new multi-subscribe feed builder instance.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EMultiSubscribeFeed.Builder} is the mechanism for
* creating an {@code EMultiSubscribeFeed} instance. A
* {@code Builder} instance is acquired from
* {@link EMultiSubscribeFeed#builder()}. The following
* example shows how to create an {@code EMultiSubscribeFeed}
* instance using a {@code Builder}. The code assumes that
* the target class implements {@code ESubscriber} interface
* methods.
* @Overricde public void startup() {
final EMultiSubscribeFeed feed = (EMultiSubscribeFeed.builder()).target(this)
.messageClass(CatalogUpdate.class)
.scope(EFeed.FeedScope.REMOTE_ONLY)
.subjects(mSubjectsList) // msSubjectsList is List<String> subject list
// Call .statusCallback(lambda expression) and
// .notifyCallback(lambda expression) to replace feedStatus, notify 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;
/**
* Feed status callback. If not explicitly set by client,
* then defaults to
* {@link ESubscriber#feedStatus(EFeedState, ESubscribeFeed)}.
*/
private FeedStatusCallback mStatusCallback;
/**
* Notification message callback. If not explicity set by
* client, then defaults to
* {@link ESubscriber#notify(ENotificationMessage, ESubscribeFeed)}.
*/
private NotifyCallback extends ENotificationMessage> mNotifyCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private Builder()
{
super (EMultiSubscribeFeed.class,
ENotificationMessage.class);
mCondition = NO_CONDITION;
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
@Override
protected Validator validate(final Validator problems)
{
// If the status and notify callbacks are not
// set, then use the interface methods if defined.
// Did the subscriber override feedStatus?
if (mStatusCallback == null &&
mTarget != null &&
isOverridden(FEED_STATUS_METHOD,
EFeedState.class,
IESubscribeFeed.class))
{
// Yes. Use the override method.
mStatusCallback =
((ESubscriber) mTarget)::feedStatus;
}
// Did the subscriber override notify?
if (mNotifyCallback == null &&
mTarget != null &&
isOverridden(NOTIFY_METHOD,
ENotificationMessage.class,
IESubscribeFeed.class))
{
// Yes. Use the override method.
mNotifyCallback =
((ESubscriber) mTarget)::notify;
}
return (super.validate(problems)
.requireNotNull(mStatusCallback,
FEED_STATUS_METHOD +
" not overridden and statusCallback not set")
.requireNotNull(mNotifyCallback,
NOTIFY_METHOD +
" not overriden and notifyCallback not set"));
} // end of validate(Validator)
@Override
protected ESubscribeFeed createFeed(EMessageKey key)
{
final ESubscribeFeed.Builder builder =
ESubscribeFeed.builder();
return (builder.target((ESubscriber) mTarget)
.location(mLocation)
.messageKey(key)
.scope(mScope)
.statusCallback(mStatusCallback)
.notifyCallback(mNotifyCallback)
.build());
}
@Override
protected EMultiSubscribeFeed buildImpl()
{
return (new EMultiSubscribeFeed(this));
} // end of buildImpl()
@Override
protected Builder self()
{
return (this);
} // end of self()
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets subscription condition to the given value. May
* be {@code null} which results in a
* {@link #NO_CONDITION} condition.
*
* An example using this method is:
*
* {@code condition(m -> ((CatalogUpdate) m).category == Category.APPLIANCES)}
* @param condition subscription 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 subscribe feed status callback in place. If
* {@code cb} is not {@code null}, subscribe status
* updates will be passed to {@code cb} rather than
* {@link ESubscriber#feedStatus(EFeedState, IESubscribeFeed)}.
* The reverse is true, if {@code cb} is {@code null},
* then updates are posted to the
* {@link ESubscriber#feedStatus(EFeedState, IESubscribeFeed)}
* override.
*
* An example using this method is:
*
* statusCallback(
(fs, f) →
{
if (fs == EFeedState.DOWN) {
// Clean up in-progress work.
}
}
* @param cb subscribe status update callback. May be
* {@code null}.
* @return {@code this Builder} instance.
*/
public Builder statusCallback(FeedStatusCallback cb)
{
mStatusCallback = cb;
return (this);
} // end of statusCallback(FeedStatusCallback<>)
/**
* Puts the notification message callback in place. If
* {@code cb} is not {@code null}, then notification
* messages will be passed to {@code cb} rather than
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed)}.
* A {@code null cb} means that notification messages will be
* passed to the
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed)}
* override.
*
* An example showing how to use this method is:
* {@code notifyCallback(this::locationUpdate)}
*
* @param notification message subclass passed into
* callback.
* @param cb pass notification messages back to target
* via this callback.
* @return {@code this Builder} instance.
*/
public Builder notifyCallback(final NotifyCallback cb)
{
mNotifyCallback = cb;
return (this);
} // end of notifyCallback()
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class Builder
} // end of class EMultiSubscribeFeed