
net.sf.eBus.client.ESubscribeFeed Maven / Gradle / Ivy
//
// Copyright 2015, 2016, 2018, 2019 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 java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import javax.annotation.Nullable;
import net.sf.eBus.client.EClient.ClientLocation;
import static net.sf.eBus.client.EFeed.NOTIFY_METHOD;
import net.sf.eBus.logging.AsyncLoggerFactory;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.Validator;
import org.slf4j.Logger;
/**
* {@code ESubscribeFeed} is the application entry point for
* receiving {@link ENotificationMessage notification messages}.
* Follow these steps to use this feed:
*
* Step 1: Implement the {@link ESubscriber}
* interface.
*
*
* Step 2:
* Use {@link ESubscribeFeed.Builder} to create a subscribe feed
* for a given {@code ESubscriber} instance and
* {@link EMessageKey type+topic message key}. The condition is
* optional and defaults to {@code null}. If provided, then only
* notification messages satisfying the condition are forwarded
* to the subscriber.
*
* Use {@link Builder#statusCallback(FeedStatusCallback)} and
* {@link Builder#notifyCallback(NotifyCallback)} to set Java
* lambda expressions used in place of {@link ESubscriber}
* interface methods.
*
*
*
* Step 3: {@link #subscribe() Subscribe} to the
* open feed.
*
*
* Step 4: Wait for an
* {@link EFeedState#UP up}
* {@link ESubscriber#feedStatus(EFeedState, IESubscribeFeed) feed status}.
* This callback will occur before any notification messages are
* delivered. If the feed state is {@link EFeedState#DOWN down},
* then no notifications will be delivered until the feed state
* comes back up.
*
*
* Step 5: Once the feed state is up, wait for
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed) notification messages}
* to arrive.
*
*
* Step 6: When the subscriber is shutting down,
* {@link #unsubscribe() retract} the subscription and
* {@link #close() close} the feed.
*
* Example use of ESubscribeFeed
* 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;
Step 1: Implement the ESubscriber interface.
public class CatalogSubscriber implements ESubscriber
{
// Subscribe to this notification message class/subject key and feed scope.
private final EMessageKey mKey;
private final FeedScope mScope;
// Store the feed here so it can be used to unsubscribe.
private ESubscribeFeed mFeed;
public CatalogSubscriber(final String subject, final FeedScope scope) {
mKey = new EMessageKey(CatalogUpdate.class, subject);
mScope = scope;
mFeed = null;
}
@Override public void startup() {
try {
Step 2: Open the ESubscribe feed.
// This subscriber has no associated ECondition and uses ESubscriber interface method overrides.
mFeed = (ESubscribeFeed.builder()).target(this)
.messageKey(mKey)
.scope(mScope)
.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 EPublisher
* @see EPublishFeed
*
* @author Charles W. Rapp
*/
public final class ESubscribeFeed
extends ENotifyFeed
implements IESubscribeFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link ESubscriber#feedStatus(EFeedState, IESubscribeFeed)}
* method name.
*/
public static final String FEED_STATUS_METHOD =
"feedStatus";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
AsyncLoggerFactory.getLogger(ESubscribeFeed.class);
//-----------------------------------------------------------
// Locals.
//
/**
* Use this condition to check if the notification message
* should be forwarded to subscriber.
*/
private final ECondition mCondition;
/**
* Feed status callback. If not explicitly set by client,
* then defaults to
* {@link ESubscriber#feedStatus(EFeedState, ESubscribeFeed)}.
*/
private final FeedStatusCallback mStatusCallback;
/**
* Notification message callback. If not explicitly set by
* client, then defaults to
* {@link ESubscriber#notify(ENotificationMessage, ESubscribeFeed)}.
*/
private final NotifyCallback extends ENotificationMessage> mNotifyCallback;
/**
* If this subscription is for the latest notification
* message, then create the task used to deliver the
* latest notification once and reuse for all message
* delivery.
*/
private final @Nullable LatestNotifyTask mLatestTask;
/**
* Use to create a {@link NotifyTask} instance used to
* forward message to subscriber.
*/
private final Function mTaskCreator;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
private ESubscribeFeed(final Builder builder)
{
super (builder);
mCondition = builder.mCondition;
mStatusCallback = builder.mStatusCallback;
mNotifyCallback = builder.mNotifyCallback;
if (builder.mLatestOnly)
{
mLatestTask =
new LatestNotifyTask(
mCondition, this, mNotifyCallback);
mTaskCreator = this::createLatestTask;
}
else
{
mLatestTask = null;
mTaskCreator = this::createTask;
}
} // end of ESubscribeFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Overrides.
//
@Override
/* package */ int updateActivation(final ClientLocation loc,
final EFeedState fs)
{
boolean updateFlag = false;
int retval = 0;
// Does this feed support the contra-feed's location?
if (mScope.supports(loc))
{
// Yes. Update the activation count based on the
// feed state.
// Increment or decrement?
if (fs == EFeedState.UP)
{
// Increment.
++mActivationCount;
retval = 1;
// Update?
updateFlag = (mActivationCount == 1);
}
// Decrement.
else if (mActivationCount > 0)
{
--mActivationCount;
retval = -1;
// Update?
updateFlag = (mActivationCount == 0);
}
// Did this feed transition between inactivation and
// activation?
if (updateFlag)
{
// Yes. Update the feed.
update(fs);
}
}
// No, this location is not supported. Do not modify the
// activation count.
sLogger.trace(
"{} subscriber {}, feed {}: {} ({}) feed state={}, activation count={} ({}), update?={} -> {}.",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
loc,
fs,
mActivationCount,
mScope,
updateFlag,
retval);
return (retval);
} // end of updateActivation(ClientLocation)
/**
* Updates the subscriber feed state. An {@code UP} feed
* state means the publisher may start posting notification
* messages to this feed as long as the publisher's feed
* state is also {@code UP}. Issues a callback to the
* client's feed state callback method.
* @param feedState latest publisher state.
*/
@Override
/* package */ void update(final EFeedState feedState)
{
sLogger.trace(
"{} subscriber {}, feed {}: update feed state={}.",
mEClient.location(),
mEClient.clientId(),
mFeedId,
feedState);
mFeedState = feedState;
// Note: the caller acquired the client dispatch before
// calling this method.
mEClient.dispatch(
new StatusTask<>(feedState, this, mStatusCallback));
} // end of update(EFeedState)
/**
* If the advertisement is in place, then retracts it.
*/
@Override
protected void inactivate()
{
// Retract the subscription in case it is in place.
unsubscribe();
} // end of inactivate()
//
// end of Abstract Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IESubscribeFeed Interface Implementation.
//
/**
* Activates this notification subscription. The caller will
* be asynchronously informed of the current feed state via
* the
* {@link ESubscriber#feedStatus(EFeedState, IESubscribeFeed) feed stateupdate callback method}.
*
* Nothing is done if already subscribed.
*
* @throws IllegalStateException
* if this feed is closed or if the client did not override
* {@link ESubscriber} methods nor put the required callbacks
* in place.
*
* @see #unsubscribe()
* @see EFeed#close()
*/
@Override
public void subscribe()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
if (!mInPlace)
{
((ENotifySubject) mSubject).subscribe(this);
// The subscription is now in place.
mInPlace = true;
}
} // end of subscribe()
/**
* De-activates this subscriber feed. Does nothing if this
* feed is not currently subscribed.
*
* Note that the client may still receive notification
* messages which were posted concurrently as this
* unsubscribe.
*
*
* @see #subscribe
* @see EFeed#close()
*/
@Override
public void unsubscribe()
{
// Is the feed subscribed?
if (mInPlace)
{
// Yes. Well, unsubscribe it.
sLogger.debug(
"{} subscriber {}, feed {}: unsubscribing from {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
((ENotifySubject) mSubject).unsubscribe(this);
// This feed is no longer subscribed ...
mInPlace = false;
// ... which means there are no more publishers.
mActivationCount = 0;
mFeedState = EFeedState.UNKNOWN;
}
} // end of unsubscribe()
//
// end of IESubscribeFeed Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* Dispatches {@code msg} to the client if the message passes
* the subscription condition.
* @param msg forward this message to the client if this
* message is acceptable.
*/
/* package */ void notify(final ENotificationMessage msg)
{
final NotifyTask task;
// Is the subscription still in place?
// Is this task in place?
if (mInPlace &&
(task = mTaskCreator.apply(msg)) != null)
{
// Yes. Post this message to the client task queue.
// The notify task will check the subscription
// condition prior to forwarding this message.
// Note: the caller acquired the client dispatch
// before calling this method.
mEClient.dispatch(task);
}
} // end of notify(ENotificationMessage)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Returns a new {@code ESubscribeFeed} builder instance.
* This instance should be used to build a single
* notification subscribe feed instance and not used to
* create multiple such feeds.
* @return new notification subscribe feed builder.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
/**
* Verifies that given parameters are not set to {@code null}
* and that {@code key} references a notification message and
* that the message scope and feed scope are in agreement.
* This method is called for effect only.
* @param client subscriber client.
* @param key notification message code.
* @param scope feed scope.
*/
protected static void validateOpenParams(final ESubscriber client,
final EMessageKey key,
final FeedScope scope)
{
// Are the parameters non-null references?
Objects.requireNonNull(client, "client is null");
Objects.requireNonNull(key, "key is null");
Objects.requireNonNull(scope, "scope is null");
// Is the message key for a notification?
if (!key.isNotification())
{
throw (
new IllegalArgumentException(
String.format(
"%s is not a notification message",
key)));
}
// Are the feed scope and message scope in agreement?
checkScopes(key, scope);
} // end of validateOpenParams(...)
/**
* Returns {@code NotifyTask} containing given notification
* message.
* @param msg wrap this notification message in a new
* notify task.
* @return new {@code NotifyTask} instance.
*/
private NotifyTask createTask(final ENotificationMessage msg)
{
return (
new NotifyTask(
msg, mCondition, this, mNotifyCallback));
} // end of createTask(ENotificationMessage)
/**
* Returns {@link #mLatestTask} instance if this
* task is not already posted to the subscriber's dispatch
* queue; otherwise this queued task's notification message
* is updated resulting in the undelivered notification being
* dropped.
* @param msg place message in latest task.
* @return latest task instance if not on queue and
* {@code null} if on queue.
*/
private @Nullable NotifyTask createLatestTask(final ENotificationMessage msg)
{
return (mLatestTask.latest(msg) ? mLatestTask : null);
} // end of createLatestTask(ENotificationMessage)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code ESubscribeFeed.Builder} is now the preferred
* mechanism for creating a {@code ESubscribeFeed} instance.
* A {@code Builder} instance is acquired from
* {@link ESubscribeFeed#builder()}. The following example
* shows how to create a {@code ESubscribeFeed} instance
* using a {@code Builder}. The code assumes that the class
* implements {@code ESubscriber} interface
* {@code feedStatus} method but uses a separate method for
* notification message callback.
* @Override public void startup() {
final EMessageKey key = new EMessageKey(CatalogUpdate.class, subject);
final ESubscribeFeed feed = (ESubscribeFeed.builder()).target(this)
.messageKey(key)
.scope(EFeed.FeedScope.REMOTE_ONLY)
.condition(m → ((CatalogUpdate) m).category == Category.APPLIANCES)
// Call .statusCallback(lambda expression) to replace feedStatus method
.notifyCallback(this::catalogUpdate)
.build();
...
}
*
* eBus release 7.3.0 introduced {@link #latestOnly(boolean)}
* property which allows a subscriber to receive only the
* latest notification in case publisher(s) post
* notifications faster than the subscriber can process them
* and the subscriber must work with the latest
* notification for correct processing.
*
*
* @see #builder()
*/
public static final class Builder
extends ENotifyFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Set to {@code true} if subscriber wishes to receive
* latest only the latest notification for the configured
* message key. Defaults to {@code false} which means all
* notifications will be delivered to the subscriber.
*/
private boolean mLatestOnly;
/**
* Use this condition to check if the notification message
* should be forwarded to subscriber. Default settings is
* {@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.
//
/**
* Creates a new subscribe feed builder.
*/
private Builder()
{
super (FeedType.SUBSCRIBE_FEED,
ESubscribeFeed.class);
mLatestOnly = false;
mCondition = NO_CONDITION;
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Checks if feed status and notify callbacks are either
* set explicitly or the {@code ESubscriber} methods are
* overridden.
* @param problems place invalid configuration settings
* in this problems list.
* @return {@code problems} to allow for method chaining.
*/
@Override
protected Validator validate(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,
"statusCallback")
.requireNotNull(mNotifyCallback,
"notifyCallback"));
} // end of validate(Validator)
/**
* Returns a new {@code ESubscribeFeed} instance based on
* {@code this Builder}'s configuration.
* @return new notification subscribe feed.
*/
@Override
protected ESubscribeFeed buildImpl()
{
final ESubscribeFeed retval =
new ESubscribeFeed(this);
// Let the client know it is being referenced by another
// feed - but only if this is not part of a multiple
// key feed. In that case, the multiple key feed is
// added to the client.
if (!mIsMultiFlag)
{
mEClient.addFeed(retval);
}
sLogger.debug(
"{} subscriber {}, feed {}: opened {} ({}).",
mLocation,
mEClient.clientId(),
retval.feedId(),
mKey,
mScope);
return (retval);
} // 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.
//
/**
* Sets latest-only notification value to given flag.
* This capability should be used when publisher(s) may
* post notifications faster than a subscriber can
* process them and the subscriber does not need
* to receive all messages but the latest notification.
* In other words, the subscriber's correct notification
* processing depends on keeping up with the latest
* update and not on processing all notifications.
*
* Default setting is for subscriber to receive all
* notifications - that is {@code false}.
*
* @param flag if {@code true} then only most recent
* notification for configured message key is delivered
* to subscriber.
* @return {@code this Builder} instance.
*/
public Builder latestOnly(final boolean flag)
{
mLatestOnly = flag;
return (this);
} // end of latestOnly(boolean)
/**
* 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(final 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
/**
* Task used to delivery latest notification message to
* subscriber.
*/
private final class LatestNotifyTask
extends NotifyTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Store latest notification message here and retrieve
* when notify callback is performed.
*/
private final AtomicReference mLatestMessage;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a notify callback task which forwards latest
* notification message to subscriber.
* @param condition subscription condition.
* @param feed message is from this feed.
* @param cb notification message callback.
*/
public LatestNotifyTask(final ECondition condition,
final IESubscribeFeed feed,
final NotifyCallback cb)
{
super (condition, feed, cb);
mLatestMessage = new AtomicReference<>();
} // end of LatestNotifyTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// NotifyTask Method Overrides.
//
/**
* Returns latest notification message stored in this
* task.
* @return latest notification message.
*/
@Override
protected @Nullable ENotificationMessage message()
{
return (mLatestMessage.getAndSet(null));
} // end of message()
//
// end of NotifyTask Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
private boolean latest(final ENotificationMessage message)
{
final ENotificationMessage previous =
mLatestMessage.getAndSet(message);
return (previous == null);
} // end of latest(ENotificationMessage)
//
// end of Set Methods.
//-------------------------------------------------------
} // end of class LatestNotifyTask
} // end of class ESubscribeFeed