
net.sf.eBus.client.EPublishFeed Maven / Gradle / Ivy
//
// Copyright 2015, 2016, 2019, 2020 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 net.sf.eBus.client.EClient.ClientLocation;
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 EPublishFeed} is the application entry point for
* publishing {@link ENotificationMessage notification messages}
* to subscribers. Follow these steps to use this feed:
*
* Step 1: Implement the {@link EPublisher}
* interface.
*
*
* Step 2:
* {@link Builder Build}
* a publish feed for a given {@code EPublisher} instance and
* {@link EMessageKey type+topic message key}.
*
*
* Use {@link Builder#statusCallback(FeedStatusCallback)}
* to set Java lamdba expression used in place of
* {@link EPublisher} interface method.
*
*
* Step 3: {@link #advertise() Advertise} this
* publisher to eBus. This allows eBus to match publishers to
* subscribers. An optional step is to
* {@link #updateFeedState(EFeedState) set the feed state}
* to {@link EFeedState#UP up} if the publisher is always able to
* publish the message.
*
*
* Step 4: Wait for the
* {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
* callback where the feed state is {@link EFeedState#UP up}.
* Attempting to publish before this will result in
* {@link #publish(ENotificationMessage)} throwing an
* {@link IllegalStateException}.
*
*
* Step 5: Start publishing notifications. Note
* that
* {@link #updateFeedState(EFeedState)} with an up feed state
* must be done prior to publishing notifications.
*
*
* Step 6: When the publisher is shutting down,
* {@link #unadvertise() retract} the notification advertisement
* and {@link #close() close} the feed.
*
* Example use of EPublishFeed
* import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.EPublisher;
import net.sf.eBus.client.EPublishFeed;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
// Step 1: Implement EPublisher interface.
public class CatalogPublisher implements EPublisher {
// Publishes this notification message class/subject key.
private final EMessageKey mKey;
// Published messages remain within this scope.
private final FeedScope mScope;
// Advertise and publish on this feed.
private EPublishFeed mFeed;
public CatalogPublisher(final String subject, final FeedScope scope) {
mKey = new EMessageKey(CatalogUpdate.class, subject);
mScope = scope;
mFeed = null;
}
@Override
public void startup() {
try {
// Step 2: Open publish feed. Place EPublisher interface method overrides here.
// This publisher overrides EPublisher interface method.
mFeed = (EPublishFeed.builder()).target(this)
.messageKey(mKey)
.scope(mScope)
.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) {
// Are we starting a feed?
if (feedState == EFeedState.UP) {
// Yes. Start publishing notifications on the feed.
startPublishing();
} else {
// We are stopping the feed.
stopPublishing();
}
}
public void updateProduct(final String productName, final Money price, final int stockQty) {
if (mFeed != null && mFeed.isFeedUp()) {
// Step 5: Start publishing notifications.
mFeed.publish((CatalogUpdate.builder()).subject(mKey.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() {
Application-specific code not shown.
}
// Stops the notification feed if up.
private void stopPublishing() {
Application-specific code not shown.
}
}
* Updating Feed State
* The reason for separating advertising and feed state is to
* support uncertain publisher feeds. Uncertain publisher feeds
* fail into two types 1) unreliable external data source and 2)
* a combination subscriber and publisher.
*
* The first might be an external device connected to a serial
* port, providing intermittent data updates. The eBus publisher
* converts that data into notification messages. If the serial
* interface is unreliable and goes down, the eBus publisher
* calls {@code updateFeedState(EFeedState.DOWN)} to inform
* subscribers about the fact.
*
*
* The second is a publisher which also subscribes to one or more
* other feeds, publishing a value-added notification based on
* the inbound notifications. If one of the subscribed feeds
* goes down, then the publisher sets its feed state to down.
* When the subscribed feed is back up, then the publisher is
* also back up.
*
*
* The above scenarios could also be accomplished by having the
* publisher {@code undadvertise()} and {@code advertise()} when
* the feed is down and up, respectively. But eBus expends much
* effort doing this. It is less effort to leave the
* advertisement in place and update the feed state.
*
*
* @author Charles W. Rapp
*/
public final class EPublishFeed
extends ENotifyFeed
implements IEPublishFeed
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link EPublisher#publishStatus(EFeedState, EPublishFeed)}
* method name.
*/
/* package */ static final String PUB_STATUS_METHOD =
"publishStatus";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
AsyncLoggerFactory.getLogger(EPublishFeed.class);
//-----------------------------------------------------------
// 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)}.
*/
private final FeedStatusCallback mStatusCallback;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
private EPublishFeed(final Builder builder)
{
super (builder);
mPublishState = EFeedState.UNKNOWN;
mStatusCallback = builder.mStatusCallback;
} // end of EPublishFeed(Builder)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Updates the feed activation count if the feed
* scope supports the contra-feed location. If the activation
* count transitions between activate and inactive, then
* updates the feed.
* @param loc contra-feed location.
* @param fs contra-feed state.
* @return 1 if activated, -1 if deactivated, and zero if
* not affected.
*/
@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;
// Return one if-and-only-if this publisher's
// feed state is up. Otherwise, return zero.
retval = (mPublishState == EFeedState.UP ? 1 : 0);
// 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(
"{} publisher client {}, feed {}: {} feed state={}, contra location={}, activation count={} ({}), update?={} -> {}.",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
fs,
loc,
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 subscriber state.
*/
@Override
/* package */ void update(final EFeedState feedState)
{
sLogger.trace(
"{} publisher {}: {} subscriber feed state is {}.",
mEClient.location(),
mFeedId,
key(),
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 feed state is up, then informs the notify subject
* that it is now down. If the advertisement is in place,
* then retracts it.
*/
@Override
protected void inactivate()
{
if (mInPlace)
{
((ENotifySubject) mSubject).unadvertise(this);
}
} // end of inactivate()
//
// end of Abstract Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IEPublishFeed Interface Implementation.
//
/**
* Returns {@code true} if this 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()} returns {@code true}.
*
* @return {@code true} if this publish feed is open and
* advertised.
*
* @see #isActive()
* @see #isFeedUp()
* @see #publishState()
*/
@Override
public boolean isAdvertised()
{
return (mIsActive.get() && mInPlace);
} // end of isAdvertised()
/**
* Returns the publish state. The returned state is not to be
* confused with {@link #feedState()} 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.
* @return current publish state.
*/
@Override
public EFeedState publishState()
{
return (mPublishState);
} // end of publishState()
/**
* Returns {@code true} if {@code subject} equals this feed's
* subject and {@link #isFeedUp()} is {@code true}. Otherwise,
* returns {@code false}.
* @param subject check if the feed for this subject is up.
* @return {@code true} if the publisher feed is up for the
* specified subject and the publisher is free to publish
* notification messages.
*/
@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));
}
// Returns true if the subject equals this single-key
// feed subject and this feed is up.
return (subject.equals((mSubject.key()).subject()) &&
this.isFeedUp());
} // end of isFeedUp(String)
/**
* Returns {@code true} if the publisher is clear to publish
* a notification 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.
* @return {@code true} if the publisher feed is up and the
* publisher is free to publish notification messages.
*
* @see #isActive()
* @see #isAdvertised()
*/
@Override
public final boolean isFeedUp()
{
return (mPublishState == EFeedState.UP &&
mFeedState == EFeedState.UP);
} // end of isFeedUp()
/**
* Advertises this publisher feed to the associated
* notification subject. If this feed is currently advertised
* to the subject, then does nothing. 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.
*
*
* 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(
"{} publisher {}, feed {}: advertising {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
mFeedState =
((ENotifySubject) mSubject).advertise(this);
// This feed is now advertised.
mInPlace = true;
}
} // end of advertise()
/**
* Retracts this publisher feed from the associated
* notification subject. Does nothing if this feed is not
* currently advertised.
* @throws IllegalStateException
* if this feed was closed and is inactive.
*
* @see #advertise()
* @see #close()
*/
@Override
public void unadvertise()
{
if (!mIsActive.get())
{
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
else if (mInPlace)
{
sLogger.debug(
"{} publisher {}, feed {}: unadvertising {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
mSubject.key(),
mScope);
((ENotifySubject) mSubject).unadvertise(this);
// This feed is no longer advertised ...
mPublishState = EFeedState.UNKNOWN;
mInPlace = false;
// ... which means there are no more subscribers.
mActivationCount = 0;
mFeedState = EFeedState.DOWN;
}
} // 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. If this feed is advertised to the server and
* the subscription feed is up, then this update is forwarded
* to the subject.
* @param update the new publish feed state.
* @throws NullPointerException
* if {@code update} is {@code null}.
* @throws IllegalStateException
* if this feed was closed and is inactive 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(
"{} publisher {}, feed {}: setting {} feed state to {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
update,
mScope);
// Forward the update to the subject.
((ENotifySubject) mSubject).updateFeedState(this);
}
// No change. Nothing to do.
} // end of updateFeedState(EFeedState)
/**
* Posts this notification message to all interested
* subscribers.
* @param msg post this message to subscribers.
* @throws NullPointerException
* if {@code msg} is {@code null}.
* @throws IllegalArgumentException
* if {@code msg} message key does not match the feed message
* key.
* @throws IllegalStateException
* if this feed is inactive, not advertised or the publisher
* has not declared the feed to be up.
*/
@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 this message key correct?
if (!(mSubject.key()).isAssignableFrom(msg.key()))
{
throw (
new IllegalArgumentException(
String.format(
"received msg key %s, expected %s",
msg.key(),
mSubject.key())));
}
// 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"));
}
doPublish(msg);
} // end of publish(ENotificationMessage)
//
// end of IEPublishFeed Interface Implementation.
//-----------------------------------------------------------
/**
* Returns a new {@code EPublishFeed} builder instance. This
* instance should be used to build a single notification
* publish feed instance and not used to create multiple
* such feeds.
* @return new notification publisher feed builder.
*/
public static Builder builder()
{
return (new Builder());
} // end of builder()
/**
* {@link ERemoteApp} calls this method to reset the remote
* feed state to {@link EFeedState#DOWN down} when the final
* subscriber is removed for this remote feed. This is due
* to the fact that the remote eBus does not forward publish
* state changes when there are no subscribers to the remote
* publishers.
*
* @see #updateFeedState(EFeedState)
*/
/* package */ void clearFeedState()
{
if (mFeedState != EFeedState.DOWN)
{
mFeedState = EFeedState.DOWN;
sLogger.debug(
"{} publisher {}, feed {}: setting {} feed state to {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
mFeedState,
mScope);
// There is no reason to inform ENotifiySubject of
// this change.
// Why?
// Because there are no subscribers interested in
// this feed.
}
} // end of setFeedState()
/**
* Performs the actual work of publishing the message to the
* subject. This method makes one last check determining if
* there are any subscribers to this publisher feed. The
* reason for this separate method is that
* {@link EMultiPublishFeed#publish(ENotificationMessage)}
* performs the same checks as {@code EPublishFeed.publish}
* except for the final subscriber feed state check.
* @param msg
*/
/* package */ void doPublish(final ENotificationMessage msg)
{
// Are there any subscribers?
// If not, quietly ignore the message. This situation
// is not an error because there is an inherit race
// condition between the publisher sending a message at
// the same time the last subscription is retracted.
if (mFeedState != EFeedState.UP)
{
sLogger.debug(
"{} publisher {}, feed {}: unable to publish {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
mScope);
}
else
{
// Everything checks out. Send the message on its
// way.
sLogger.debug(
"{} publisher {}, feed {}: publishing {} ({}).",
mEClient.location(),
mEClient.clientId(),
mFeedId,
key(),
mScope);
((ENotifySubject) mSubject).publish(msg, this);
}
} // end of doPublish(ENotificationMessage)
/**
* Verifies that meet the required feed opening constraints;
* if this verification fails, then the appropriate
* exception is thrown. Otherwise returns. This method is
* called for effect only.
* @param client application object publishing notification
* messages. May not be {@code null}.
* @param key notification message class and subject.
* May not be {@code null} and must reference a notification
* message class.
* @param scope whether the feed supports local feeds, remote
* feeds, or both. May not be {@code null}.
* @throws NullPointerException
* if any of the arguments are {@code null}.
* @throws IllegalArgumentException
* if any of the arguments is invalid.
*/
protected static void validateOpenParams(final EPublisher 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(EPublisher,EMessageKey,FeedScope)
//---------------------------------------------------------------
// Inner classes.
//
/**
* {@code EPublishFeed.Builder} is now the preferred
* mechanism for creating a {@code EPublishFeed} instance.
* A {@code Builder} instance is acquired from
* {@link EPublishFeed#builder()}. The following example
* shows how to create a {@code EPublishFeed} instance using
* a {@code Builder}. The code assumes that the class
* implements {@code EPublisher} interface
* {@code feedStatus} method.
*
@Override public void startup() {
final EMessageKey key = new EMessageKey(CatalogUpdate.class, subject);
final EPublishFeed feed = (EPublishFeed.builder()).target(this)
.messageKey(key)
.scope(EFeed.FeedScope.LOCAL_AND_REMOTE)
// Call .statusCallback(lambda expression) to replace feedStatus method
.build();
...
}
*
* @see #builder()
*/
public static final class Builder
extends ENotifyFeed.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.
//
/**
* Creates a notification publish feed builder.
*/
private Builder()
{
super (FeedType.PUBLISH_FEED, EPublishFeed.class);
} // end of Builder()
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Checks to see if either feed status callback was set
* explicitly or {@code EPublisher.publishStatus} is
* overridden.
* @param problems place invalid configuration settings
* in this problems list.
* @return {@code problems} to allow for method chaining.
*/
@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,
"statusCallback"));
} // end of validate(Vaidator)
/**
* Returns a new {@code EPublishFeed} instance based on
* {@code this Builder}'s configuration.
* @return new notification publish feed.
*/
@Override
protected EPublishFeed buildImpl()
{
final EPublishFeed retval = new EPublishFeed(this);
if (!mIsMultiFlag)
{
mEClient.addFeed(retval);
}
sLogger.debug(
"{} publisher {}, 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.
//
/**
* 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 EPublishFeed