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

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

There is a newer version: 7.6.0
Show newest version
//
// Copyright 2017, 2020, 2023 Charles W. Rapp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package net.sf.eBus.client;

import java.util.Objects;
import static net.sf.eBus.client.EPublishFeed.PUB_STATUS_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.Validator;

/**
 * This feed allows an {@link EPublisher} to open one feed for a
 * given notification message class and multiple message subjects.
 * It acts as a proxy between the publisher and the individual,
 * subordinate feeds. The publisher interacts solely with the
 * multi-subject publisher. The publisher client opens,
 * advertises, un-advertises, and closes the multi-subject feed.
 * In turn, the multi-subject feed opens, advertises,
 * un-advertises, and closes the subordinate
 * {@link EPublishFeed}s in unison But the subordinate
 * feeds issue
 * {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)}
 * callbacks to the {@code EPublisher} registered with the
 * multi-subject feed. The multi-subject feed does not callback
 * to the publisher client. If the client opens a large number of
 * subordinate feeds, then the client must be prepared for a
 * large number of callbacks.
 * 

* Subordinate feeds are selected by passing a notification * message class to * {@link EMultiFeed.Builder#messageClass(java.lang.Class) EMultiPublishFeed.Builder.messageKey} * and either a subject list to * {@link EMultiFeed.Builder#subjects(java.util.List) EMultiPublishFeed.Builder.subjects} * or a regular express query to * {@link EMultiFeed.Builder#query(net.sf.eBus.util.regex.Pattern) EMultiPublishFeed.Builder.query}. * The first limits the subordinate feeds to exactly those whose * message key is listed. The second chooses message keys * with the given message class and whose subjects match the * regular expression. In either case, the publisher may * {@link #addFeed(String) add} or * {@link #closeFeed(String) remove} feeds dynamically while the * multi-subject feed is open. When adding a new publish feed, * the new feed is configured in the same was as existing feeds * and put into the same state (open, advertised, etc.). *

*

Example use of EMultiPublishFeed

*
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.EMultiPublishFeed;
import net.sf.eBus.client.EPublisher;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.util.regex.Pattern;

// Step 1: Implement EPublisher interface.
public class CatalogPublisher implements EPublisher {
    // Select subjects matching this query pattern.
    private final Pattern mQuery;

    // Published message scope.
    private final FeedScope mScope;

    // Advertise and publish on this feed.
    private EMultiPublishFeed mFeed;

    public CatalogPublisher(final Pattern query, final FeedScope scope) {
        mQuery = query;
        mScope = scope;
    }

    @Override
    public void startup() {
        try {
            // Step 2: Open multi-subject publish feed. Place EPublisher interface method overrides here.
            // This publisher overrides EPublisher interface method.
            mFeed = (EMultiPublishFeed.builder()).target(this)
                                                 .scope(mScope)
                                                 .messageClass(CatalogUpdate.class)
                                                 .query(mQuery)
                                                 .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) {
        final String subject = (feed.key()).subject();

        // Are we starting a feed?
        if (feedState == EFeedState.UP) {
            // Yes. Start publishing notifications on the feed.
            startPublishing(subject);
        } else {
            // We are stopping the feed.
            stopPublishing(subject);
        }
    }

    public void updateProduct(final String subject, final String productName, final Money price, final int stockQty) {
        if (mFeed != null && mFeed.isFeedUp(subject)) {
            // Step 5: Start publishing notifications.
            mFeed.publish((CatalogUpdate.builder()).subject(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(final String subject) {
         Application-specific code not shown.
    }

    // Stops the notification feed if up.
    private void stopPublishing(final String subject) {
        Application-specific code not shown.
    }
}
* * @see EPublisher * @see EMultiSubscribeFeed * * @author Charles W. Rapp */ public final class EMultiPublishFeed extends EMultiFeed implements IEPublishFeed { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // 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)}. * This callback is applied to each subordinate * {@code EPublishFeed}. */ private final FeedStatusCallback mStatusCallback; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // private EMultiPublishFeed(final Builder builder) { super (builder); mPublishState = EFeedState.UNKNOWN; mStatusCallback = builder.mStatusCallback; } // end of EMultiPublishFeed(Builder) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // IEPublishFeed Interface Implementations. // /** * Returns {@code true} if this multikey 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(String)} returns {@code true}. *

* @return {@code true} if this publish feed is open and * advertised. * * @see #isActive() * @see #isFeedUp(String) * @see #feedState(String) */ @Override public boolean isAdvertised() { return (mIsActive.get() && mInPlace); } // end of isAdvertise() /** * Returns the publish state. The returned state is not to be * confused with {@link #feedState(String)} 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. *

* An up publish state does not mean that the publisher is * clear to post notifications to the feed. See * {@link #isFeedUp(String)} to determine if the publisher * may post notifications to a specific subject. *

* @return current publish state. * * @see #isFeedUp(String) */ @Override public EFeedState publishState() { return (mPublishState); } // end of publishState() /** * Returns {@code true} if the publisher is clear to publish * a notification for the given feed 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. *

* Returns {@code false} if {@code key} does not reference * a known message feed. *

* @param subject check the feed status for the feed * referenced by this subject. * @return {@code true} if the specified publisher feed is up * and the publisher is free to publish notification * messages for the specified subject. * @throws NullPointerException * if {@code subject} is {@code null}. * @throws IllegalArgumentException * if {@code subject} is empty. * @throws IllegalStateException * if this multi-subject feed is closed. * * @see #isActive() * @see #isAdvertised() */ @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)); } final EPublishFeed feed = mFeeds.get(subject); return (feed != null && feed.isFeedUp()); } // end of isFeedUp(String) /** * Advertises each subordinate {@link EPublishFeed}. If this * feed is currently advertised, then does nothing. The * publisher client will receive a * {@link EPublisher#publishStatus(EFeedState, IEPublishFeed)} * callback for each subordinate publish feed. *

* 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. Note this * feed state is applied to all subordinate feeds. It is * not necessary for the publisher to call set the feed * state for individual feeds. *
  • *
*

* 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( "{} multi-subject publisher {}: advertising ({}).", mEClient.location(), mEClient.clientId(), mScope); // Advertise each subordinate feed. mFeeds.values() .stream() .forEachOrdered(EPublishFeed::advertise); // This feed is now advertised. mInPlace = true; // If this multi-subject feed is query based, then // listen for subject updates. if (mQuery != null) { ESubject.addListener(this); } } } // end of advertise() /** * Retracts this multi-subject publisher feed by * un-advertising each subordinate publish feed. Does nothing * if this feed is not currently advertised. * @throws IllegalStateException * if this multi-subject publisher feed is closed. * * @see #advertise() * @see #close() */ @Override public void unadvertise() { if (!mIsActive.get()) { throw (new IllegalStateException(FEED_IS_INACTIVE)); } if (mInPlace) { sLogger.debug( "{} multi-subject publisher {}: unadvertising ({}).", mEClient.location(), mEClient.clientId(), mScope); // Unadvertise each subordinate feed. mFeeds.values() .stream() .forEachOrdered(EPublishFeed::unadvertise); // This feed is no longer advertised. mPublishState = EFeedState.UNKNOWN; mInPlace = false; // Stop listening for subject updates. ESubject.removeListener(this); } } // end of unadvertise() /** * Updates the publish feed state for this multi-feed and all * message keys to the given value. If {@code update} equals * the currently stored multi-feed publish feed state, * nothing is done. Otherwise, the updated value is stored * and the subordinate publish feed states are updated as * well. *

* The publish feed state may be updated only when this feed * is open and advertised. The method may not be called when * the feed is closed or un-advertised. *

* @param update the new publish feed state. * @throws NullPointerException * if {@code update} is {@code null}. * @throws IllegalStateException * if this feed was closed 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( "{} multi-subject publisher {}: setting feed state to {} ({}).", mEClient.location(), mEClient.clientId(), update, mScope); // Update each subordinate publish feed. mFeeds.values() .forEach(feed -> feed.updateFeedState(update)); } } // end of updateFeedState(EFeedState) /** * Posts a notification message to all subscribers via the * subordinate publish feed which matches the message's key. * @param msg post this message to the matching subordinate * feed. * @throws NullPointerException * if {@code msg} is {@code null}. * @throws IllegalArgumentException * if {@code msg} message key does not reference a known * subordinate publish feed. * @throws IllegalStateException * if this feed is inactive, not advertised, the publisher * has not declared the feed to be up, or there are no * subscribers listening to the subordinate feed. */ @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 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")); } // Does the message reference a known subordinate feed? final String subject = (msg.key()).subject(); if (!mFeeds.containsKey(subject)) { // No. throw ( new IllegalArgumentException( subject + " is an unknown feed")); } // So far, so good. Pass this message to the subordinate // feed and let it do its thing. (mFeeds.get(subject)).doPublish(msg); } // end of publish(ENotificationMessage) // // end of IEPublishFeed Interface Implementations. //----------------------------------------------------------- //----------------------------------------------------------- // Abstract Method Implementations. // /** * Returns a newly minted subordinate publish feed for the * given key. The returned feed's configuration is the same * as existing subordinate feeds. * @param key create feed for this key. * @return a subordinate publish feed. */ @Override protected EPublishFeed createFeed(final EMessageKey key) { final EPublishFeed.Builder builder = EPublishFeed.builder(); return (builder.target((EPublisher) mEClient.target()) .location(location()) .messageKey(key) .scope(mScope) .statusCallback(mStatusCallback) .build()); } // end of createFeed(EMessageKey) /** * Sets the feed status callback, advertises {@code feed}, * and updates the publish feed state. The publish feed state * is taken from the most recent * {@link #updateFeedState(EFeedState)} setting. If the * publish feed state has never been updated, then the state * is {@link EFeedState#UNKNOWN}. * @param feed advertise this feed. * * @see #updateFeedState(EFeedState) */ @Override protected void putFeedInPlace(final EPublishFeed feed) { feed.advertise(); feed.updateFeedState(mPublishState); } // end of putFeedInPlace(EPublishFeed) // // end of Abstract Method Implementations. //----------------------------------------------------------- /** * Returns a new multi-publish feed builder. It is * recommended that a new {@code Builder} instance be used * for each {@code EMultiPublishFeed} creation. * @return new multi-publish feed builder instance. */ public static Builder builder() { return (new Builder()); } // end of builder() /** * Updates publish feed state for a single subject in the * multi-feed publisher. The overall multi-feed publish feed * state is left unchanged. *

* The publish feed state may be updated only when this feed * is open and advertised. The method may not be called when * the feed is closed or un-advertised. *

* @param subject subject currently in multi-feed. * @param update new publish feed state. * @throws NullPointerException * if {@code subject} or {@code update} is {@code null}. * @throws IllegalStateException * if this feed was closed or is not advertised. * @throws IllegalArgumentException * if {@code subject} is not part of the multi-feed. */ public void updateFeedState(final String subject, final EFeedState update) { Objects.requireNonNull(subject, "subject is null"); Objects.requireNonNull(update, "update is null"); if (!mIsActive.get()) { throw (new IllegalStateException(FEED_IS_INACTIVE)); } else if (!mInPlace) { throw ( new IllegalStateException(FEED_NOT_ADVERTISED)); } else if (!mFeeds.containsKey(subject)) { throw ( new IllegalArgumentException( subject + " is not part of this multi-feed")); } else { (mFeeds.get(subject)).updateFeedState(update); } } // end of updateFeedState(String, EFeedState) //--------------------------------------------------------------- // Inner classes. // /** * {@code EMultiPublishFeed.Builder} is the mechanism for * creating an {@code EMultiPublishFeed} instance. A * {@code Builder} instance is acquired from * {@link EMultiPublishFeed#builder()}. The following * example shows how to create an {@code EMultiPublishFeed} * instance using a {@code Builder}. The code assumes that * the target class implements {@code EPublisher} interface * {@code feedStatus} method. *
@Overricde public void startup() {
    final EMultiPublishFeed feed = (EMultiPublishFeed.builder()).target(this)
                                                     .messageClass(CatalogUpdate.class)
                                                     .scope(EFeed.FeedScope.LOCAL_AND_REMOTE)
                                                      // Call .statusCallback(lambda expression) to replace feedStatus method
                                                     .build();
    ...
}
*/ public static final class Builder extends EMultiFeed.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. // private Builder() { super (EMultiPublishFeed.class, ENotificationMessage.class); } // end of Builder() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Abstract Method Overrides. // @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, PUB_STATUS_METHOD + " not overridden and statusCallback not set")); } // end of validate(Validator) @Override protected EPublishFeed createFeed(final EMessageKey key) { final EPublishFeed.Builder builder = EPublishFeed.builder(); return (builder.target((EPublisher) mTarget) .location(mLocation) .messageKey(key) .scope(mScope) .statusCallback(mStatusCallback) .build()); } // end of createFeed(EMessageKey) @Override protected EMultiPublishFeed buildImpl() { return (new EMultiPublishFeed(this)); } // 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 EMultiPublishFeed




© 2015 - 2025 Weber Informatics LLC | Privacy Policy