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

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

The newest version!
//
// Copyright 2017, 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.EFeed.NO_CONDITION;
import static net.sf.eBus.client.EReplyFeed.CANCEL_METHOD;
import static net.sf.eBus.client.EReplyFeed.REQUEST_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.util.Validator;

/**
 * This feed allows an {@link EReplier} to open one feed for a
 * given request message class and multiple message subjects. It
 * acts as a proxy between the replier and the individual,
 * subordinate feeds. The replier interacts solely with the
 * multi-subject reply feed and is unable to access underlying
 * {@code EReplyFeed}s. The replier client opens, advertises,
 * un-advertises, and closes the multi-subject feed. In turn, the
 * multi-subject feed opens, advertises, un-advertises, and
 * closes subordinate {@link EReplyFeed}s. But
 * subordinate feeds issue
 * {@link EReplier#request(EReplyFeed.ERequest)}
 * callbacks to the {@code EReplier} registered with the
 * multi-subject feed. The multi-subject feed does not callback
 * to the replier client. If the client opens a large number of
 * subordinate feeds, then the client must be prepared for a
 * large number of callbacks.
 * 

* The subordinate feeds are selected by passing a reply message * message class to * {@link EMultiFeed.Builder#messageClass(java.lang.Class) EMultiReplyFeed.Builder.messageClass} * and either a subject list to * {@link EMultiFeed.Builder#subjects(java.util.List) EMultiReplyFeed.Builder.subject} * or * a regular express query to * {@link EMultiFeed.Builder#query(net.sf.eBus.util.regex.Pattern) EMultiReplyFeed.Builder.query}. * The first limits the subordinate feeds to exactly those whose * subject is listed. The second chooses 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 reply 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 EMultiReplyFeed

*
import java.util.ArrayList;
import java.util.List;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EReplier;
import net.sf.eBus.client.EReplyFeed;
import net.sf.eBus.messages.EReplyMessage
import net.sf.eBus.messages.EReplyMessage.ReplyStatus; import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.util.regex.Pattern;

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

    private final FeedScope mScope;
    private EMultiReplyFeed mFeed;
    private final List<EReplyFeed.ERequest> mRequests;

    public CatalogReplier(final Pattern query, final FeedScope scope) {
        mQuery = query;
        mScope = scope;
        mRequests = new ArrayList<>();
    }

    @Override
    public void startup() {
        try {
            Step 2: Open a reply feed for reply message key. May override EReplier interfaces methods.
            // This advertisement has no associated ECondition and uses EReplier interface method overrides.
            mFeed = (EMultiReplyFeed.builder()).target(this)
                                               .scope(mScope)
                                               .messageClass(com.acme.CatalogOrder.class)
                                               .query(mQuery)
                                               .build();

            Step 3: Advertise reply feed.
            mFeed.advertise();

            mFeed.updateFeedState(EFeedState.UP);
        } catch (IllegalArgumentException argex) {
            // Advertisement failed. Place recovery code here.
        }
    }

    Step 4: Wait for requests to arrive.
    @Override
    public void request(final EReplyFeed.ERequest request, final EReplyFeed feed) {
        final ERequestMessage msg = request.request();

        try {
            mRequests.add(request);
            startOrderProcessing(msg, request);
        } catch (Exception jex) {
            request.reply(new CatalogOrderReply(ReplyStatus.ERROR, // reply status.
                                                 jex.getMessage())); // reply reason.
        }
    }

    @Override
    public void cancelRequest(final EReplyFeed.ERequest request, final boolean mayRespond) {
        // Is this request still active? It is if the request is listed.
        if (mRequests.remove(request)) {
            // Yes, try to stop the request processing.
            try {
                // Throws an exception if the request cannot be stopped.
                stopOrderProcessing(request);
            } catch (Exception jex) {
                // Ignore since nothing else can be done.
            }
        }
    }

    Step 5: Send one or more reply messages back to requestor.
    public void orderReply(final EReplyFeed.ERequest request, final ReplyStatus status, final String reason) {
        final ERequestAd ad = mRequests.get(request);

        if (mRequests.contains(request) && request.isActive()) {
            request.reply((OrderReply.builder()).subject(mKey.subject())
                                                .timestamp(Instant.now())
                                                .replyStatus(status)
                                                .replyReason(reason)
                                                .build());

            // If the request processing is complete, remove the request.
            if (status.isFinal()) {
                mRequests.remove(request);
            }
        }
    }

    @Override
    public void shutdown() {
        final String subject = mKey.subject();

        // While eBus will does this for us, it is better to do it ourselves.
        for (EReplyFeed.ERequest request : mRequests) {
            request.reply((EReplyMessage.builder()).subject(subject)
                                                   .timestamp(Instant.now())
                                                   .replyStatus(ReplyStatus.ERROR)
                                                   .replyReason("shutting down")
                                                   .build());
        }

        mRequests.clear();

        Step 6: When shutting down, either unadvertise or close reply feed.
        if (mFeed != null) {
            mFeed.close();
            mFeed = null;
        }
    }
}
* * @see EReplier * @see ERequestor * @see EMultiRequestFeed * * @author Charles W. Rapp */ public final class EMultiReplyFeed extends EMultiFeed implements IEReplyFeed { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Locals. // /** * Use this condition to check if the request message should * be forwarded to subscriber. */ private final ECondition mCondition; /** * Tracks the replier's ability to handle request messages. * for this feed. {@link #mFeedState} tracks whether there * are any requestor to this feed. */ private EFeedState mReplyState; /** * Contains the functional interface callback for request * messages. If not explicitly set by client, then defaults * to * {@link EReplier#request(EReplyFeed.ERequest)}. */ private final RequestCallback mRequestCallback; /** * Contains the functional interface callback for request * messages. If not explicitly set by client, then defaults * to * {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}. */ private final CancelRequestCallback mCancelCallback; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new multi-reply feed instance based on * validated builder settings. * @param builder contains multi-reply feed settings. */ private EMultiReplyFeed(final Builder builder) { super (builder); mCondition = builder.mCondition; mRequestCallback = builder.mRequestCallback; mCancelCallback = builder.mCancelCallback; mReplyState = EFeedState.UNKNOWN; } // end of EMultiReplyFeed(Builder) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // IEReplyFeed Interface Implementations. // /** * Returns {@code true} if this reply feed is both open and * advertised; otherwise, returns {@code false}. * @return {@code true} if this reply feed is open and * advertised. */ @Override public boolean isAdvertised() { return (mIsActive.get() && mInPlace); } // end of isAdvertised() /** * Advertises each subordinate {@link EReplyFeed}. If this * feed is currently advertised, then does nothing. If the * cancel request and request callbacks were previously set * for this feed, then these callbacks are set in the * subordinate feeds prior to advertising them. * @throws IllegalStateException * if this feed is closed or the client did not override * {@link EReplier} methods nor put the required callbacks * 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 replier {}: advertising ({}).", mEClient.location(), mEClient.clientId(), mScope); // Advertise each subordinate feed. mFeeds.values() .stream() .forEachOrdered(EReplyFeed::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 replier feed by un-advertising * each subordinate reply feed. Does nothing if this * feed is not currently advertised. * @throws IllegalStateException * if this multi-subject reply 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 replier {}: unadvertising ({}).", mEClient.location(), mEClient.clientId(), mScope); // Unadvertise each subordinate feed. mFeeds.values() .stream() .forEachOrdered(EReplyFeed::unadvertise); // This feed is no longer advertised. mReplyState = EFeedState.UNKNOWN; mInPlace = false; // Stop listening for subject updates. ESubject.removeListener(this); } } // end of unadvertise() /** * Updates the reply 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 and the subordinate reply feed states are updated * as well. *

* The reply 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 reply 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 != mReplyState) { // Yes. Apply the update. mReplyState = update; sLogger.debug( "{} multi-subject replier {}: 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) // // end of IEReplyFeed Interface Implementations. //----------------------------------------------------------- //----------------------------------------------------------- // Abstract Method Implementations. // /** * Returns a newly minted subordinate reply feed for the * given key. * @param key create feed for this key. * @return a subordinate reply feed. */ @Override protected EReplyFeed createFeed(EMessageKey key) { final EReplyFeed.Builder builder = EReplyFeed.builder(); return (builder.target((EReplier) mEClient.target()) .location(location()) .messageKey(key) .scope(mScope) .condition(mCondition) .requestCallback(mRequestCallback) .cancelRequestCallback(mCancelCallback) .build()); } // end of createFeed(EMessageKey) /** * Sets the feed status callback, advertises {@code feed}, * and updates the reply feed state. The reply feed state * is taken from the most recent * {@link #updateFeedState(EFeedState)} setting. If the * reply feed state has never been updated, then the state * is {@link EFeedState#UNKNOWN}. * @param feed advertise this feed. */ @Override protected void putFeedInPlace(final EReplyFeed feed) { feed.advertise(); feed.updateFeedState(mReplyState); } // end of putFeedInPlace(EReplyFeed) // // end of Abstract Method Implementations. //----------------------------------------------------------- //----------------------------------------------------------- // Get Methods. // /** * Returns the reply state which specifies whether this * multi-subject reply feed (and its subordinate feeds) are * ready to handle requests or not. An update reply state * does not mean that the replier will receive requests but * only that the replier is capable of handling requests at * this time. * @return current reply state. * * @see #feedState(String) */ public EFeedState replyState() { return (mReplyState); } // end of replyState() // // end of Get Methods. //----------------------------------------------------------- /** * Returns a new {@code EMultiReplyFeed} builder instance. * This instance should be used to build a single reply feed * instance and not used to create multiple such feeds. * @return new feed builder. */ public static Builder builder() { return (new Builder()); } // end of builder() //--------------------------------------------------------------- // Inner classes. // /** * {@code EMultiReplyFeed.Builder} is the mechanism for * creating an {@code EMultiReplyFeed} instance. A * {@code Builder} instance is acquired from * {@link EMultiReplyFeed#builder()}. The following example * shows how to create an {@code EMultiReplyFeed} instance * using a {@code Builder}. The code assumes that the target * class implements {@code EReplier} interface methods. *
@Overricde public void startup() {
    final EMultiReplyFeed feed = (EMultiReplyFeed.builder()).target(this)
                                                            .messageClass(com.acme.CatalogOrder.class)
                                                            .scope(EFeed.FeedScope.REMOTE_ONLY)
                                                            .subjects(mSubjectsList) // msSubjectsList is List<String> subject list
                                                            // Call .requestCallback(lambda expression) and
                                                            // .cancelRequestCallback(lambda expression) to replace request, cancelRequest methods
                                                            .build();
    ...
}
*/ public static final class Builder extends EMultiFeed.Builder { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Apply this condition to all incoming messages. Only * those messages satisfying the condition are forwarded * to the target. Defaults to {@link #NO_CONDITION}. */ private ECondition mCondition; /** * Contains the functional interface callback for request * messages. If not explicitly set by client, then * defaults to * {@link EReplier#request(EReplyFeed.ERequest)}. */ private RequestCallback mRequestCallback; /** * Contains the functional interface callback for request * messages. If not explicitly set by client, then * defaults to * {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}. */ private CancelRequestCallback mCancelCallback; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new multi-feed reply builder with the * given "require callbacks" flag. */ private Builder() { super (EMultiReplyFeed.class, ERequestMessage.class); mCondition = NO_CONDITION; } // end of Builder() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Abstract Method Overrides. // @Override protected Validator validate(final Validator problems) { // If the callbacks were not put in place, then use // the defaults. if (mRequestCallback == null && mTarget != null && isOverridden(REQUEST_METHOD, EReplyFeed.ERequest.class)) { mRequestCallback = ((EReplier) mTarget)::request; } if (mCancelCallback == null && mTarget != null && isOverridden(CANCEL_METHOD, EReplyFeed.ERequest.class, boolean.class)) { mCancelCallback = ((EReplier) mTarget)::cancelRequest; } return (super.validate(problems) .requireNotNull(mRequestCallback, REQUEST_METHOD + " not overridden or requestCallback not set") .requireNotNull(mCancelCallback, CANCEL_METHOD + " not overridden or cancelRequestCallback not set")); } // end of validate(Validator) @Override protected EReplyFeed createFeed(final EMessageKey key) { final EReplyFeed.Builder builder = EReplyFeed.builder(); return (builder.target((EReplier) mTarget) .location(mLocation) .messageKey(key) .scope(mScope) .requestCallback(mRequestCallback) .cancelRequestCallback( mCancelCallback) .build()); } // end of createFeed(EMessageKey) @Override protected EMultiReplyFeed buildImpl() { return (new EMultiReplyFeed(this)); } // end of buildImpl() @Override protected Builder self() { return (this); } // end of self() // // end of Abstract Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // /** * Sets advertisement condition to the given value. May * be {@code null} which results in condition being set * to {@link #NO_CONDITION}. *

* An example using this method is: *

* {@code condition(m -> ((CatalogUpdate) m).category == Category.APPLIANCES)} * @param condition advertisement condition. * @return {@code this Builder} instance. */ public Builder condition(final ECondition condition) { if (condition == null) { mCondition = NO_CONDITION; } else { mCondition = condition; } return (this); } // end of condition(ECondition) /** * Puts the new request callback in place. If {@code cb} * is not {@code null}, requests will be passed to * {@code cb} rather than * {@link EReplier#request(EReplyFeed.ERequest)}. A * {@code null cb} means that requests are passed to the * {@link EReplier#request(EReplyFeed.ERequest)} * override. *

* An example using this method is: *

* {@code requestCallback(this::newOrder)} * @param cb the request callback. May be {@code null}. * @return {@code this Builder} instance. */ public Builder requestCallback(final RequestCallback cb) { mRequestCallback = cb; return (this); } // end of requestCallback(RequestCallback) /** * Puts the cancel request callback in place. If * {@code cb} is not {@code null}, requests will be * passed to {@code cb} rather than * {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)}. * A {@code null cb} means that cancellations are passed * to the * {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)} * override. *

* An example using this method is: *

* {@code this::cancelOrder} * @param cb the cancel request callback. May be * {@code null}. * @return {@code this Builder} instance. */ public Builder cancelRequestCallback(final CancelRequestCallback cb) { mCancelCallback = cb; return (this); } // end of cancelRequestCallback(CancelRequestCallback) // // end of Set Methods. //------------------------------------------------------- } // end of class Builder } // end of class EMultiReplyFeed




© 2015 - 2024 Weber Informatics LLC | Privacy Policy