All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.eBus.client.EPublishFeed Maven / Gradle / Ivy

The newest version!
//
// 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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy