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

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

//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright 2015, 2016. Charles W. Rapp
// All Rights Reserved.
//

package net.sf.eBus.client;

import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;

/**
 * {@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 #open(EPublisher, EMessageKey, EFeed.FeedScope) Open} * a publish feed for a given {@code EPublisher} instance and * {@link EMessageKey type+topic message key}. *

*

* Step 3 (optional): Do not override * {@link EPublisher} interface method. Instead, set callback * using {@link #statusCallback(FeedStatusCallback)} passing in a * Java lambda expression. *

*

* Step 4: {@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 5: 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 6: Start publishing notifications. Note * that * {@link #updateFeedState(EFeedState)} with an up feed state * must be done prior to publishing notifications. *

*

* Step 7: When the publisher is shutting down, * {@link #unadvertise() retract} the notification advertisement * and {@link #close() close} the feed. *

*

Example use of {@code 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 {
    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.
            mFeed = EPublishFeed.open(this, mKey, mScope);

            // Step 3: Overriding EPublisher interface methods.

            // Step 4: 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 5: Handle publish status update.
    @Override
    public void publishStatus(final EFeedState feedState, final EPublishFeed feed) {
        EFeedState publishState;

        // Are we starting a feed?
        if (feedState == EFeedState.UP) {
            // Yes. Start publishing notifications on the feed.
            publishState = 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 6: Start publishing notifications.
            mFeed.publish(new CatalogUpdate(productName, price, stockQty));
        }
    }

    // Retract the notification feed.
    @Override
    public void shutdown() {
        Step 7: 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.
    }

    // 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;
}
*

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 = Logger.getLogger(EPublishFeed.class.getName()); //----------------------------------------------------------- // 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 FeedStatusCallback mStatusCallback; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a publish feed for the given client, scope, and * notification subject. * @param client {@link EPublisher publisher} client. * @param scope the {@link FeedScope feed scope}. * @param subject the type+topic notification subject. */ private EPublishFeed(final EClient client, final FeedScope scope, final ENotifySubject subject) { super (client, scope, FeedType.PUBLISH_FEED, subject); mPublishState = EFeedState.UNKNOWN; mStatusCallback = null; } // end of EPublishFeed(...) // // 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. if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format("%s client %d, feed %d: %s feed state=%s, activation count=%d (%s), update?=%b -> %d.", mEClient.location(), mEClient.clientId(), mFeedId, key(), 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 subscriber state. */ @Override /* package */ void update(final EFeedState feedState) { if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format("%s publisher %d: %s subscriber feed state is %s.", 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)); return; } // 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); // Remove from the advertiser list if the feed is // both a local client and either a local & remote // or remote feed. if (mEClient.isLocal() && (mScope == FeedScope.LOCAL_AND_REMOTE || mScope == FeedScope.REMOTE_ONLY)) { mAdvertisers.remove(this); } } return; } // end of inactivate() // // end of Abstract Method Overrides. //----------------------------------------------------------- //----------------------------------------------------------- // Get Methods. // /** * 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 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 boolean isFeedUp() { return (mPublishState == EFeedState.UP && mFeedState == EFeedState.UP); } // end of isFeedUp() /** * 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-key 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) // // end of Get Methods. //----------------------------------------------------------- /** * Returns a notification publish feed for the specified * notification message class and subject. * @param client the application object publishing the * notification. May not be {@code null}. * @param key the 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. * @return a new notification feed for the given application * object and message key. * @throws NullPointerException * if any of the arguments are {@code null}. * @throws IllegalArgumentException * if any of the arguments is invalid. * * @see #statusCallback(FeedStatusCallback) * @see #advertise() */ public static EPublishFeed open(final EPublisher client, final EMessageKey key, final FeedScope scope) { // Validate the parameters. // 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))); } return (open(client, key, scope, ClientLocation.LOCAL, false)); } // end of open(Object, EMessageKey, boolean, EPublisher) /** * 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}. That * is, a {@code null cb} means publish status updates are * posted to the * {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)} * override. * @param cb the publish status update callback. May be * {@code null}. * @throws IllegalStateException * if this feed is either closed or advertised. * * @see #advertise() */ @Override public void statusCallback(final FeedStatusCallback cb) { if (!mIsActive.get()) { throw ( new IllegalStateException("feed is inactive")); } if (mInPlace) { throw ( new IllegalStateException( "advertisement in place")); } mStatusCallback = cb; return; } // end of statusCallback(FeedStatusCallback<>) /** * 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) { if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s publisher %d, feed %d: advertising %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, mSubject.key(), mScope)); } // Was a publish status callback put in place? if (mStatusCallback == null) { // No. Did the publisher override EPublisher // methods? if (!isOverridden(PUB_STATUS_METHOD, EFeedState.class, IEPublishFeed.class)) { // No? Gotta do one or the other. throw ( new IllegalStateException( PUB_STATUS_METHOD + " not overridden and statusCallback not set")); } // Create a callback back to // EPublisher.publishStatus() method. mStatusCallback = ((EPublisher) mEClient.target())::publishStatus; } mFeedState = ((ENotifySubject) mSubject).advertise(this); // Add to the advertiser list if the client is local // and the supports remote feeds. if (mEClient.isLocal() && (mScope == FeedScope.LOCAL_AND_REMOTE || mScope == FeedScope.REMOTE_ONLY)) { mAdvertisers.add(this); } // This feed is now advertised. mInPlace = true; } return; } // 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) { if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s publisher %d, feed %d: unadvertising %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, mSubject.key(), mScope)); } ((ENotifySubject) mSubject).unadvertise(this); // Remove from the advertiser list if the client is // local and the supports remote feeds. if (mEClient.isLocal() && (mScope == FeedScope.LOCAL_AND_REMOTE || mScope == FeedScope.REMOTE_ONLY)) { mAdvertisers.remove(this); } // This feed is no longer advertised ... mPublishState = EFeedState.UNKNOWN; mInPlace = false; // ... which means there are no more subscribers. mActivationCount = 0; mFeedState = EFeedState.DOWN; } return; } // 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; if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s publisher %d, feed %d: setting %s feed state to %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, key(), update, mScope)); } // Forward the update to the subject. ((ENotifySubject) mSubject).updateFeedState(this); } // No change. Nothing to do. return; } // 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 (!(msg.key()).equals(mSubject.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); return; } // end of publish(ENotificationMessage) /** * Returns a notification publish feed for the specified * notification message class and subject. *

* This method does not parameter validation since this is * a {@code package private} method. *

* @param cl the application object publishing the * notification. May not be {@code null}. * @param key the 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. * @param l {@code client} location. * @param isMulti {@code true} if this is part of a multiple * key feed. If {@code true}, this feed is not added to the * client feed list. * @return a new notification feed for the given application * object and message key. */ public static EPublishFeed open(final EPublisher cl, final EMessageKey key, final FeedScope scope, final ClientLocation l, final boolean isMulti) { final EClient eClient; final ENotifySubject subject; final EPublishFeed retval; // Find ore open the eBus client wrapping client. eClient = EClient.findOrCreateClient(cl, l); // Lastly, find or open the notification subject. subject = ENotifySubject.findOrCreate(key); // The arguments check-out, so far. Create the publish // feed. retval = new EPublishFeed(eClient, scope, subject); // 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 (!isMulti) { eClient.addFeed(retval); } if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s publisher %d, feed %d: opened %s (%s).", eClient.location(), eClient.clientId(), retval.feedId(), key, scope)); } return (retval); } // end of open(...) /** * {@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; if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s publisher %d, feed %d: setting %s feed state to %s (%s).", 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. } return; } // 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.DOWN) { sLogger.info( String.format("%s publisher %d, feed %d: unable to publish %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, key(), mScope)); } else { // Everything checks out. Send the message on its // way. if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s publisher %d, feed %d: publishing %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, key(), mScope)); } ((ENotifySubject) mSubject).publish(msg, this); } return; } // end of doPublish(ENotificationMessage) } // end of class EPublishFeed




© 2015 - 2025 Weber Informatics LLC | Privacy Policy