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

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

The newest version!
//
// Copyright 2015, 2016, 2019 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.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.EReplyMessage.ReplyStatus;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBus.messages.type.MessageType;
import net.sf.eBus.util.MultiKey2;
import net.sf.eBus.util.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@code ERequestFeed} is the application entry point for
 * posting {@link ERequestMessage request messages} to repliers.
 * Follow these steps to use this feed:
 * 

* Step 1: Implement the {@link ERequestor} * interface. *

*

* Step 2: * Use {@link ERequestFeed.Builder} to create a request feed * instance for a given {@code ERequestor} instance and * {@link EMessageKey type+topic message key}. *

*

* Use {@link Builder#statusCallback(FeedStatusCallback)}, * {@link Builder#replyCallback(ReplyCallback)} and/or * {@link Builder#replyCallback(java.lang.Class, ReplyCallback)} * to set Java lambda expression used in place of * {@link ERequestor} interface methods. *

*

* Step 3: * {@link #subscribe() Subscribe} to reply message key. *

*

* Step 4: * {@link #request(ERequestMessage) Send} a request to one or * more advertised repliers. If there are no advertised repliers, * then {@code request(ERequestMessage)} returns * {@link RequestState#DONE}, signaling that the requestor will * not receive any replies. *

*

* Step 5: Wait for * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest) replies} * to arrive. When the {@code remaining} parameter is zero, that * means this is the final reply from all repliers. When * {@link EReplyMessage#replyStatus} is * {@link ReplyStatus#ERROR} or {@link ReplyStatus#OK_FINAL}, * then this is the final reply from that replier. *

*

* Step 6: A requestor may terminate a request * prior to completion by calling * {@link ERequestFeed.ERequest#close()}. No more replies should * be received once this is done. (Note: there is a possibility * that an in-flight reply will still be posted to the * requestor.) *

*

* Step 7: When * requestor is shutting down, {@link #unsubscribe() retract} the * subscription and {@link #close close} the feed. *

*

Example use of ERequestFeed

*
import java.util.ArrayList;
import java.util.List;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.ERequestFeed;
import net.sf.eBus.client.ERequestor;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;

Step 1: Implement the ERequestor interface.
public class CatalogRequestor implements ERequestor {
    private final EMessageKey mKey;
    private final FeedScope mScope;
    private final List<ERequestFeed.ERequest> mRequests;
    private ERequestFeed mFeed;

    public CatalogRequestor(final String subject, final FeedScope scope) {
        mKey = new EMessageKey(com.acme.CatalogOrder.class, subject);
        mScope = scope;
        mRequests = new ArrayList<>();
    }

    @Override
    public void startup() {
        try {
            Step 2: Open request feed. May override ERequestor interface methods.
            mFeed = (ERequestFeed.builder()).target(this)
                                            .messageKey(mKey)
                                            .scope(mScope)
                                            .builder();

            Step 3: Subscribe to reply message key.
            mFeed.subscribe();
        } catch (IllegalArgumentException argex) {
            // Open failed. Place recovery code here.
        }
    }

    @Override
    public void shutdown() {
        synchronized (mRequests) {
            for (ERequestFeed.ERequest request : mRequests) {
                Step 6: Cancel request by closing request.
                request.close();
            }

            mRequests.clear();
        }

        Step 7: On shutdown, close request feed.
        if (mFeed != null) {
            mFeed.close();
            mFeed = null;
        }
    }

    @Override
    public void feedStatus(final EFeedState feedState, final ERequestFeed feed) {
        if (feedState == EFeedState.DOWN) {
            // Down. There are no repliers.
        } else {
            // Up. There is at least one replier.
        }
    }

    Step 4: Wait for replies to request.
    @Override
    public void reply(final int remaining,
                      final EReplyMessage reply,
                      final ERequestFeed.ERequest request) {
        final String reason = msg.replyReason();

        if (msg.replyStatus == EReplyMessage.ReplyStatus.ERROR)
        {
            // The replier rejected the request. Report the reason
        } else if (msg.replyStatus == EReplyMessage.ReplyStatus.OK_CONTINUING) {
            // The replier will be sending more replies.
        } else {
            // This is the replier's last reply.
        }

        if (remaining == 0) {
            synchronized (mRequests) {
                mRequests.remove(request);
            }
        }
    }

    Step 5: Send a request message.
    public void placeOrder(final String product,
                           final int quantity,
                           final Price price,
                           final ShippingEnum shipping,
                           final ShippingAddress address) {
        final CatalogOrder msg = (CatalogOrder.builder()).subject(mKey.subject())
                                                         .timestamp(Instant.now())
                                                         .product(product)
                                                         .price(price)
                                                         .shipping(shipping)
                                                         .address(address)
                                                         .build();

        try {
            synchronized (mRequests) {
                mRequests.add(mFeed.request(msg));
            }
        } catch (Exception jex) {
            // Request failed. Put recovery code here.
        }
    }
}
* * @see ERequestor * @see EReplier * @see EReplyFeed * * @author Charles Rapp */ public final class ERequestFeed extends ESingleFeed implements IERequestFeed { //--------------------------------------------------------------- // Enums. // /** * A request is either not placed, active, done, or canceled. */ public enum RequestState { /** * Request instantiated but not yet placed. */ NOT_PLACED, /** * Request message sent to repliers and is waiting for * reply messages. When the final reply message is * received, the request goes to the {@link #DONE} state. * Goes to the {@link #CANCELED} state if the * requestor calls * {@link ERequestFeed.ERequest#close()}. */ ACTIVE, /** * The request is no longer active because all replies * are received. The request feed may not be * used to send a new request message. */ DONE, /** * The request was canceled by client and will no * longer respond to replies. The request feed may * not be used to send a new request message. */ CANCELED } // end of RequestState //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * {@link ERequestor#feedStatus(EFeedState, IERequestFeed)} * method name. */ public static final String FEED_STATUS_METHOD = "feedStatus"; /** * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)} * method name. */ public static final String REPLY_METHOD = "reply"; //----------------------------------------------------------- // Statics. // /** * Logging subsystem interface. */ private static final Logger sLogger = LoggerFactory.getLogger(ERequestFeed.class); //----------------------------------------------------------- // Locals. // /** * The repliers actively responding to this request feed. * Repliers are added when new repliers subscribe and * removed when existing repliers un-subscribe. */ private final List mRepliers; /** * Contains the functional interface callback for feed * status updates. If not explicitly set by client, then * defaults to * {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}. */ private final FeedStatusCallback mStatusCallback; /** * Maps the reply message key to the functional interface * callback for that reply. If a reply message is not * explicitly set by the client, then defaults to * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. */ private final Map, ReplyCallback> mReplyCallbacks; /** * Set to {@code true} if {@link ERequest#close()} may * be called and {@code false} if not. */ private final boolean mCloseFlag; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new request feed based on builder settings. * @param builder contains request feed settings. */ private ERequestFeed(final Builder builder) { super (builder); mRepliers = new ArrayList<>(); mStatusCallback = builder.mStatusCallback; mReplyCallbacks = builder.mReplyCallbacks; mCloseFlag = builder.mCloseFlag; } // end of ERequestFeed(Builder) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // EFeed Interface Implementation. // /** * Cancels this feed's active requests */ @Override protected synchronized void inactivate() { ((ERequestSubject) mSubject).unsubscribe(this); } // end of inactivate() @Override /* package */ int updateActivation(final ClientLocation loc, final EFeedState fs) { 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. boolean updateFlag = false; // Increment or decrement? if (fs == EFeedState.UP) { // Increment. ++mActivationCount; retval = 1; // 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. mFeedState = fs; sLogger.debug( "{} requestor {}, feed {}: setting {} feed state to {} ({}).", mEClient.location(), mEClient.clientId(), mFeedId, key(), fs, mScope); mEClient.dispatch( new FeedStatusTask( fs, this, mStatusCallback)); } } // No, this location is not supported. Do not modify the // activation count. sLogger.trace( "{} requestor {}, feed {}: {} feed state={}, activation count={} ({}).", mEClient.location(), mEClient.clientId(), mFeedId, key(), fs, mActivationCount, mScope); return (retval); } // end of updateActivation(ClientLocation, EFeedState) // // end of EFeed Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Get Methods. // /** * Returns {@code true} if a request may be unilaterally * canceled by calling {@link ERequest#cancel()} and * {@code false} if not. * @return {@code true} if requests may be unilaterally * canceled. */ public boolean mayClose() { return (mCloseFlag); } // end of mayClose() // // end of Get Methods. //----------------------------------------------------------- //----------------------------------------------------------- // Set Methods. // /** * Puts the reply message callback in place * for all reply types. If {@code cb} is * not {@code null}, replies will be passed to {@code cb} * rather than * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. * A {@code null cb} results in replies posted to the * {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)} * override. *

* Note that this method call overrides all previous calls * to {@link #replyCallback(Class, ReplyCallback)}. If * the goal is to use a generic callback for all replies * except one specific message, then use this method to put * the generic callback in place first and then use * {@code replyCallback(EMessageKey, ReplyCallback)}. *

* @param cb reply message callback. May be {@code null}. * @throws IllegalStateException * if this feed is either closed or subscribed. * * @see #replyCallback(Class, ReplyCallback) */ public void replyCallback(final ReplyCallback cb) { if (!mIsActive.get()) { throw ( new IllegalStateException(FEED_IS_INACTIVE)); } if (mInPlace) { throw ( new IllegalStateException( "subscription in place")); } mReplyCallbacks.entrySet() .forEach(entry -> entry.setValue(cb)); } // end of replyCallback(ReplyCallback) /** * Sets the callback for a specific reply message class. If * {@code cb} is not {@code null}, replies will be passed to * {@code cb} rather than * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. * A {@code cb} results in replies posted to the * {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)} * override. *

* If the goal is to set a single callback method for all * reply message types, then use * {@link #replyCallback(ReplyCallback)}. Note that method * overrides all previous set reply callbacks. *

* @param mc the reply message class. * @param cb callback for the reply message. * @throws NullPointerException * if {@code mc} is {@code null}. * @throws IllegalArgumentException * if {@code mc} is not a reply for this request. * @throws IllegalStateException * if this feed is either closed or subscribed. */ public void replyCallback(final Class mc, final ReplyCallback cb) { Objects.requireNonNull(mc, "mc is null"); if (!mReplyCallbacks.containsKey(mc)) { throw ( new IllegalArgumentException( mc.getSimpleName() + " is not a " + mSubject.key() + " reply")); } if (!mIsActive.get()) { throw ( new IllegalStateException(FEED_IS_INACTIVE)); } if (mInPlace) { throw ( new IllegalStateException( "subscription in place")); } mReplyCallbacks.put(mc, cb); } // end of replyCallback(Class, ReplyCallback) /** * Sets the reply callbacks to the given mappings. This * method is used by {@link EMultiRequestFeed} to set * the reply callback map in one call. * @param cbs reply message callback map. */ /* package */ void replyCallbacks(Map, ReplyCallback> cbs) { mReplyCallbacks.putAll(cbs); } // end of replyCallbacks(Map<>) // // end of Set Methods. //----------------------------------------------------------- /** * Returns a new {@code ERequestFeed} builder instance. * This instance should be used to build a single * request feed instance and not used to create multiple * such feeds. * @return new feed builder. */ public static Builder builder() { return (new Builder()); } // end of builder() /** * Subscribes this request feed to the eBus subject. There is * no callback associated with request subscription. This * subscription must be in place before * {@link #request(ERequestMessage)} may be called. * @throws IllegalStateException * if this feed is closed or the client did not override * {@link ERequestor} methods nor put the required callback * in place. * * @see #unsubscribe() * @see EFeed#close() */ @Override public void subscribe() { if (!mIsActive.get()) { throw (new IllegalStateException(FEED_IS_INACTIVE)); } if (!mInPlace) { final boolean replyOverride = isOverridden(REPLY_METHOD, int.class, EReplyMessage.class, ERequestFeed.ERequest.class); sLogger.debug( "{} requestor {}, feed {}: subscribing to {} ({}).", mEClient.location(), mEClient.clientId(), mFeedId, mSubject.key(), mScope); mReplyCallbacks.entrySet() .stream() .filter( entry -> (entry.getValue() == null)) .forEachOrdered( entry -> { // Did the client override // generic reply? if (!replyOverride) { // Nope. Not much point in // putting this subscription // in place. throw ( new IllegalStateException( REPLY_METHOD + " not overridden and replyCallback not set")); } entry.setValue( ((ERequestor) mEClient.target())::reply); }); ((ERequestSubject) mSubject).subscribe(this); mInPlace = true; } // Else nothing to do since the subscription is in place. } // end of subscribe() /** * Retracts this request feed from the associated subject. * Does nothing if this feed is not currently subscribed. * This feed may be re-subscribed after un-subscribing. *

* Note that un-subscribing does not cancel any active * requests or prevent delivery of replies to those requests. *

* * @see #subscribe() * @see EFeed#close() */ @Override public void unsubscribe() { // Is there a subscription to retract? if (mInPlace) { // Yes. Retract the subscription. sLogger.debug( "{} requestor {}, feed {}: unsubscribing from {} ({}).", mEClient.location(), mEClient.clientId(), mFeedId, mSubject.key(), mScope); ((ERequestSubject) mSubject).unsubscribe(this); mInPlace = false; mActivationCount = 0; mFeedState = EFeedState.DOWN; } } // end of unsubscribe() /** * Forwards the request to all matching repliers, returning * the request instance. Throws {@code IllegalStateException} * if no matching repliers are found and, consequently, no * replies will be received. Use the returned * {@link ERequest} instance to query the request state or * cancel the request. *

* Note: once the {@link ERequest} is * returned, the application is responsible for tracking * all active request instances. This request feed does * not store or track requests on behalf of the * application. {@link #close() Closing} this requests feed * does not automatically cancel active requests created by * this feed. The application is responsible for canceling * active requests. *

* @param msg send this request message to the repliers. * @return the resulting request. * @throws NullPointerException * if {@code msg} is {@code null}. * @throws IllegalArgumentException * if {@code msg} does not match the request subject key. * @throws IllegalStateException * if there are currently no repliers for this request. * * @see ERequestFeed.ERequest#close() */ public ERequest request(final ERequestMessage msg) { // Validate parameters. Objects.requireNonNull(msg, "msg is null"); // Is the message class assignable from the key message // class? // Is this subject correct? if (!(mSubject.key()).isAssignableFrom(msg.key())) { throw ( new IllegalArgumentException( String.format( "received msg key {}, expected {}", msg.key(), mSubject.key()))); } // Is this feed still alive? if (!mIsActive.get()) { throw (new IllegalStateException("feed is closed")); } // Is the feed subscription in place? if (!mInPlace) { throw (new IllegalStateException("not subscribed")); } return (doRequest(msg)); } // end of request(ERequestMessage) /** * Finds the repliers matching the request message and * returns the request generated for the request process. The * caller is expected to verify the request message and feed * state prior to calling this method. This method is * provided since * {@link EMultiRequestFeed#request(ERequestMessage)} * performs the same checks as * {@link ERequestFeed#request(ERequestMessage)}. * @param msg request message. * @return the resulting request. */ /* package */ ERequest doRequest(final ERequestMessage msg) { // Are there any active repliers? if (mActivationCount == 0) { // No. throw ( new IllegalStateException( "no repliers for request")); } // Everything checks out. Send the request on its way. final Map repliers = new HashMap<>(mRepliers.size()); EReplyFeed.ERequest replyRequest; int replierCount = 0; sLogger.debug( "{} request {}: forwarding request to {} repliers.", mEClient.location(), mFeedId, mRepliers.size()); // Create the request first, then update its // repliers. final ERequest retval = new ERequestFeed.ERequest(this, mReplyCallbacks, mCloseFlag); // Send the request message to each replier together // with an ERequest instance and collect the // returned reply feed request instance. for (EReplyFeed replier : mRepliers) { replyRequest = replier.request(retval, msg); // Check if the reply request is not null 'cuz // null is returned if the replier is either // inactive or did not accept the request. if (replyRequest != null) { replierCount += replyRequest.remaining(); repliers.put(replier, replyRequest); } } // Are there any repliers for this request. if (replierCount > 0) { // Yes. Update the request with the repliers and // reply count. retval.repliers(replierCount, repliers.values()); // Have the replier dispatch the request *after* // the EReplyFeed.ERequest is stored away. // Otherwise, there is a chance the reply will // arrive before replierCount is set. repliers.entrySet() .stream() .forEach( entry -> (entry.getKey()).dispatch( entry.getValue())); ESubject.exhaust(msg); } else { // No. Let the user know that this request went no // where. // Be sure to close the request before throwing up. retval.close(); throw ( new IllegalStateException( "no repliers for request")); } return (retval); } // end of doRequest(ERequestMessage) /** * Adds the reply feed to the replier list if the * replier's location matches this requestor's scope. *

* Note: the caller has already determine that the replier * scope is not {@link FeedScope#REMOTE_ONLY}. *

* @param location replier location. * @param feed add this reply feed if capable */ @SuppressWarnings ({"java:S1067"}) /* package */ synchronized void addReplier(final ClientLocation location, final EReplyFeed feed) { // Add the replier if the replier's local and scope // matches this local requestor's scope. // Note: the caller has already determined that the // replier scope is *not* REMOTE_ONLY. if ((mScope == FeedScope.LOCAL_ONLY && location == ClientLocation.LOCAL) || mScope == FeedScope.LOCAL_AND_REMOTE || (mScope == FeedScope.REMOTE_ONLY && location == ClientLocation.REMOTE)) { // Add the feed to the list. mRepliers.add(feed); // If the replier feed state is up, then update the // activation count. if (feed.feedState() == EFeedState.UP) { ++mActivationCount; // Is this the first active replier? if (mActivationCount == 1) { // Yes. Inform the requestors that this // feed is up. mFeedState = EFeedState.UP; mEClient.dispatch( new FeedStatusTask( mFeedState, this, mStatusCallback)); } } } } // end of addReplier(ClientLocation, EReplyFeed) /** * Removes a reply feed from the repliers list. * @param feed remove this reply feed. */ /* package */ synchronized void removeReplier(final EReplyFeed feed) { // Remove the replier from the feed list even though the // activation count may be zero. That is because the // replier may be in the list but its feed state is down. // Make sure the replier's feed state is update before // decrementing the activation count. if (mRepliers.remove(feed) && feed.feedState() == EFeedState.UP && mActivationCount > 0) { // Removing --mActivationCount; // If the feed no longer has any repliers, then // inform the requestor client of that fact. if (mActivationCount == 0) { mFeedState = EFeedState.DOWN; mEClient.dispatch( new FeedStatusTask( mFeedState, this, mStatusCallback)); } } } // end of removeReplier(EReplyFeed) /** * Returns a reply callback map containing all the reply * message keys but with {@code null} callbacks. These * callbacks will either be set by the application or * defaulted to * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)} * interface method. * @param key request message key. * @return reply callback map. */ /* package */ static Map, ReplyCallback> createReplyCallbacks(final MessageType mt) { final List> replyClasses = mt.replyTypes(); final Map, ReplyCallback> retval = new HashMap<>(replyClasses.size()); replyClasses.forEach(clazz -> retval.put(clazz, null)); return (retval); } // end of createReplyCallbacks(Class) //--------------------------------------------------------------- // Inner classes. // /** * {@code EREquestFeed.Builder} is now the preferred * mechanism for creating a {@link ERequestFeed} instance. A * {@code Builder} instance is acquired from * {@link ERequestFeed#builder()}. The following example * shows how to create an {@code ERequestFeed} instance using * a {@code Builder}. The code assumes that the class * implements {@code ERequestor} interface {@code feedStatus} * method but uses other methods for reply messages. The * example {@code CatalogOrder} request message supports two * replies besides the generic {@code EReplyMessage}: * {@code OrderRejectReply} and {@code OrderUpdateReply}. *
@Override public void startup() {
    final EMessageKey key = new EMessageKey(com.acme.CatalogOrder.class, subject);

    final ERequestFeed feed = (ERequestFeed.builder()).target(this)
                                                      .messageKey(key) Message key must be set prior to setting reply callbacks.
                                                      .scope(EFeed.FeedScope.REMOTE_ONLY)
                                                      // Call .statusCallback(lambda expression) to replace feedStatus method
                                                      // Set different callback for each reply message class.
                                                      .replyCallback(EReplyMessage.class, this::genericReply)
                                                      .replyCallback(OrderRejectReply.class, this::orderReject)
                                                      .replyCallback(OrderUpdateReply.class, this::orderUpdate)
                                                      .build();
    ...
}
* * @see #builder() */ public static final class Builder extends ESingleFeed.Builder { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Set to {@code true} if {@link ERequest#close()} may * be called and {@code false} if not. *

* Defaults to {@code true}. *

*/ private boolean mCloseFlag; /** * Contains the functional interface callback for feed * status updates. If not explicitly set by client, then * defaults to * {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}. */ private FeedStatusCallback mStatusCallback; /** * Maps the reply message key to the functional interface * callback for that reply. If a reply message is not * explicitly set by the client, then defaults to * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. */ private Map, ReplyCallback> mReplyCallbacks; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // private Builder() { super (FeedType.REQUEST_FEED, ERequestFeed.class); mCloseFlag = true; } // end of Builder() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Abstract Method Overrides. // /** * Returns eBus subject for the configured message key. * @return eBus feed subject. */ @Override protected ESubject getSubject() { return (ERequestSubject.findOrCreate(mKey)); } // end of getSubject() @Override protected Validator validate(final Validator problems) { final boolean replyOverride = isOverridden(REPLY_METHOD, int.class, EReplyMessage.class, ERequestFeed.ERequest.class); // If callbacks not explicitly set, then set them to // the defaults. if (mStatusCallback == null && mTarget != null && isOverridden(FEED_STATUS_METHOD, EFeedState.class, IERequestFeed.class)) { mStatusCallback = ((ERequestor) mTarget)::feedStatus; } if (replyOverride && mTarget != null) { final ReplyCallback replyCb = ((ERequestor) mTarget)::reply; mReplyCallbacks.entrySet() .stream() .filter(e -> e.getValue() == null) .forEachOrdered( e -> e.setValue(replyCb)); } super.validate(problems) .requireTrue((mKey != null && mKey.isRequest()), "messageKey", "messageKey is not a request") .requireTrue((!mKey.isLocalOnly() || mScope == FeedScope.LOCAL_ONLY), "messageKey", String.format( "%s is local-only but feed scope is %s", mKey, mScope)) .requireNotNull(mStatusCallback, "statusCallback"); checkReplyCallbacks(problems); return (problems); } // end of validate(Validator) /** * Returns a new {@code ERequrestFeed} instance based on * {@code this Builder}'s configuration. * @return new request feed. */ @Override protected ERequestFeed buildImpl() { return (new ERequestFeed(this)); } // end of buildImpl() /** * Returns {@code this} reference. * @return {@code this} reference. */ @Override protected Builder self() { return (this); } // end of self() // // end of Abstact Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // /** * Sets eBus message key defining this feed. Returns * {@code this Builder} instance so that configuration * methods may be chained. * @param key eBus message key. * @return {@code this Builder} instance. * @throws NullPointerException * if {@code target} is {@code null}. */ @Override public Builder messageKey(final EMessageKey key) { // Throws NullPointerException if key is null. super.messageKey(key); // Message key is set. Now fill in the reply callback // map. mReplyCallbacks = createReplyCallbacks( (MessageType) DataType.findType( key.messageClass())); return (this); } // end of messageKey(EMessageKey) /** * Sets "may close" flag to given value. A {@code true} * means requester has ability to close an active * request; {@code false} means requester may * not cancel an active request and must wait * for the request to complete. *

* Defaults to {@code true}. *

* @param flag {@code true} means request may be closed. * @return {@code this Builder} instance. */ public Builder mayClose(final boolean flag) { mCloseFlag = flag; return (self()); } // end of mayClose(boolean) /** * Puts the feed status callback in place. If {@code cb} is * not {@code null}, feed status updates will be passed * to {@code cb} rather than * {@link ERequestor#feedStatus(EFeedState, IERequestFeed)}. * A {@code null cb} results in feed status updates * posted to the * {@link ERequestor#feedStatus(EFeedState, IERequestFeed)} * override. *

* The following example shows how to use this method: *

*
statusCallback(
    (fs, f) →
    {
        if (fs == EFeedState.DOWN) {
            // Clean up in-progress work.
        }
    }
* @param cb feed 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<>) /** * Sets the callback for a specific reply message class. * If {@code cb} is not {@code null}, replies will be * passed to {@code cb} rather than * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. * A {@code cb} results in replies posted to the * {@code ERequestor.reply(int, EReplyMessage, ERequestFeed.ERequest)} * override. *

* If the goal is to set a single callback method for all * reply message types, then use * {@link #replyCallback(ReplyCallback)}. Note that * method overrides all previous set reply callbacks. *

*

* The following example shows how to use this method * to set a reply callback lambda expression: *

* {@code OrderRejectReply.class, this::orderReject} * @param mc the reply message class. * @param cb callback for the reply message. * @return {@code this Builder} instance. * @throws NullPointerException * if {@code mc} is {@code null}. * @throws IllegalArgumentException * if {@code mc} is not a reply for this request. * @throws IllegalStateException * if {@link #messageKey(EMessageKey)} was not * successfully called prior to setting the reply * callback. * * @see #messageKey(EMessageKey) * @see #replyCallback(ReplyCallback) */ public Builder replyCallback(final Class mc, final ReplyCallback cb) { // Has the message key been set? if (mKey == null) { throw ( new IllegalStateException( "messageKey must be set adding replyCallback")); } Objects.requireNonNull(mc, "mc is null"); if (!mReplyCallbacks.containsKey(mc)) { throw ( new IllegalArgumentException( mc.getSimpleName() + " is not a " + mKey + " reply")); } mReplyCallbacks.put(mc, cb); return (this); } // end of replyCallback(Class, ReplyCallback) /** * Sets reply callback for all message classes. This * will override any previous calls to * {@link #replyCallback(Class, ReplyCallback)} will be * overwritten by this method. Therefore this method * should be called prior to setting specific reply * message class callbacks. *

* If {@code cb} is not {@code null}, replies will be * passed to {@code cb} rather than * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. * A {@code cb} results in replies posted to the * {@code ERequestor.reply(int, EReplyMessage, ERequestFeed)}. *

*

* If the goal is to set a callback for a specific reply * message class, then use * {@link #replyCallback(Class, ReplyCallback)}. *

*

* The following example shows how to set a generic * reply callback lambda expression: *

* {@code replyCallback(this::orderReply)} * @param cb callback for the reply message. * @return {@code this Builder} instance. * @throws IllegalStateException * if {@link #messageKey(EMessageKey)} was not * successfully called prior to setting the reply * callback. * * @see #replyCallback(Class, ReplyCallback) */ public Builder replyCallback(final ReplyCallback cb) { // Has the message key been set? if (mKey == null) { throw ( new IllegalStateException( "messageKey must be set adding replyCallback")); } for (Map.Entry, ReplyCallback> e : mReplyCallbacks.entrySet()) { e.setValue(cb); } return (this); } // end of replyCallback(ReplyCallback) // // end of Set Methods. //------------------------------------------------------- /** * Checks if all possible reply callbacks are set. * Missing callbacks are reported in {@code problems}. * @param problems place reply callback problems into * this list. */ private void checkReplyCallbacks(final Validator problems) { final boolean replyOverride = isOverridden(REPLY_METHOD, int.class, EReplyMessage.class, ERequestFeed.ERequest.class); mReplyCallbacks.entrySet() .stream() .filter(e -> (e.getValue() == null)) .forEachOrdered( entry -> { // Did the client override // generic reply? if (!replyOverride) { // Nope. problems.addError( new MultiKey2<>( "replyCallback", "missing " + entry.getKey() + " callback")); } }); } // end of checkReplyCallbacks(Validator) } // end of class Builder /** * This class represents an individual request, tracking the * current request state and the remaining repliers. This * class acts as a conduit from the {@link EReplier} to the * {@link ERequestor}, routing replies back to the requestor. *

* A requestor can cancel an in-progress request by calling * {@link #close()}. Once canceled, no new replies * will be delivered to the requestor. However, in-flight * replies already scheduled for delivery will be delivered. *

*

* Note: Applications are responsible for * tracking active requests once instantiated. * {@link ERequestFeed} does not track active requests for * the application. *

*/ public static final class ERequest extends ESingleFeed { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * The repliers actively responding to this request. When * a replier completes its response, the replier is * removed from this list. When the list is empty, the * request state is {@link RequestState#DONE}. *

* Maps the replier's client identifier to its request * instance. *

*/ private final List mRepliers; /** * Pass replies to application via this callback. */ private final Map, ReplyCallback> mReplyCallbacks; /** * Set to {@code true} if {@link ERequest#close()} may * be called and {@code false} if not. */ private final boolean mCloseFlag; /** * Tracks the number of remaining repliers. This tally * may be > {@code _repliers.size()} because a single * remote {@code EReplyFeed.ERequest} stands in for * multiple repliers. */ private int mRemaining; /** * The request current state. Initialized to * {@link RequestState#NOT_PLACED}. */ private RequestState mRequestState; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new request for the given replier * @param feed request feed placing this request. * @param cbs reply message key to reply callback method * map. * @param closeFlag {@code false} if {@link #close()} may * not be called. * @param cxlReq non-{@code null} if this request can be * canceled via an asynchronous message. */ private ERequest(final ERequestFeed feed, final Map, ReplyCallback> cbs, final boolean closeFlag) { super (feed.mEClient, feed.mScope, feed.mFeedType, feed.mSubject); mRepliers = new ArrayList<>(); mRemaining = 0; mRequestState = RequestState.NOT_PLACED; mReplyCallbacks = cbs; mCloseFlag = closeFlag; } // end of ERequest(ERequestFeed, Map<>, boolean) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // EFeed Abstract Method Overrides. // /** * If this request is still active, then automatically * cancels this request with all extant repliers. No * further replies will be delivered to the requestor * unless previously scheduled for delivery. */ @Override protected synchronized void inactivate() { // Is the request active or cancel-in-progress? if (mRequestState == RequestState.ACTIVE) { // Yes. Tell the remaining repliers that this // request is finished. mRepliers.forEach(req -> req.close()); // Clear out the repliers list and mark this // request as canceled. mRepliers.clear(); setState(RequestState.CANCELED); } } // end of inactivate() @Override /* package */ int updateActivation(final ClientLocation loc, final EFeedState fs) { return (0); } // end of updateActivation(ClientLocation, EFeedState) /** * Cancels this request immediately and unilaterally if * this action is allowed. If not allowed then an * {@code IllegalStateException} is thrown. *

* Does nothing if this request is not active. *

*

* If this request may not be unilaterally canceled then * it may allow for a cancel request message to * be sent to the replier(s). If all repliers accept the * cancel request then this request is then canceled. But * if at least one replier rejects the cancel request * then the request is still active. *

* @throws IllegalStateException * if either this request may not be directly canceled. */ @Override public void close() { // Is this request still alive? if (mRequestState == RequestState.ACTIVE) { // Yes. But may it be unilaterally closed? if (!mCloseFlag) { throw ( new IllegalStateException( "request may not be canceled")); } // Clear to go ahead and close this request. super.close(); } } // end of close() // // end of Abstract Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return (String.format("%s request %d", mEClient.location(), mFeedId)); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns the request current state. * @return request current state. */ public RequestState requestState() { return (mRequestState); } // end of requestState() /** * Returns the number of repliers still replying to the * request. Will be zero if the request state is * {@link RequestState#NOT_PLACED} or * {@link RequestState#DONE}. * @return number of in-progress repliers. */ public int repliersRemaining() { return (mRemaining); } // end of repliersRemaining() /** * Returns {@code true} if a request may be unilaterally * canceled by calling {@link ERequest#cancel()} and * {@code false} if not. * @return {@code true} if requests may be unilaterally * canceled. */ public boolean mayClose() { return (mCloseFlag); } // end of mayClose() // // end of Get Methods. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // /** * Sets the number of repliers still processing the * request. * @param count number of remaining repliers. May * be > {@code repliers.size()}. * @param repliers repliers. */ /* package */ void repliers(final int count, final Collection repliers) { mRepliers.addAll(repliers); mRemaining = count; // The request is now active. setState(RequestState.ACTIVE); sLogger.debug( "{}: {} remaining (active={}, state={})", this, mRemaining, mIsActive.get(), mRequestState); } // end of repliers(int, Collection<>) // // end of Set Methods. //------------------------------------------------------- /** * Attempts to cancel this request with extant repliers. * Unlike {@link #close()} a replier may reject this * attempt at canceling the request thus keeping the * request alive. This method of terminating a request * should be preferred over {@code close()} since it * allows the replier to respond to the cancel request. * @throws IllegalStateException * if this request is not active. */ public void cancel() { // Is this request still alive? if (mRequestState != RequestState.ACTIVE) { throw ( new IllegalStateException( "request is not active")); } // Yes, forward the cancel request to all repliers. mRepliers.forEach(req -> req.cancel()); } // end of cancel() /** * Forwards the reply message to the request client. If * this is a final reply from {@code replier}, then the * replier is removed from the repliers map. * @param remaining number of repliers remaining. Will be * zero for a local replier. * @param msg a reply message to this request. * @param replier the reply is from this reply feed. */ @SuppressWarnings ("unchecked") /* package */ void reply(final int remaining, final EReplyMessage msg, final EReplyFeed.ERequest replier) { final boolean isActive = mIsActive.get(); sLogger.debug( "{}: {} reply, {} final, {} remaining (active={}, state={})", this, replier.location(), (msg.isFinal() ? "is" : "is not"), remaining, isActive, mRequestState); // Is this feed still alive? // Is this request still active? if (isActive && mRequestState == RequestState.ACTIVE) { final Class mc = (Class) (msg.key()).messageClass(); // Is this the replier's final reply? if (msg.isFinal()) { // Yes. Remove the replier from the list. mRepliers.remove(replier); // Is this a *local* replier? if (replier.location() == ClientLocation.LOCAL) { // Yes. Remove it from the replier count. // Note: remote repliers update their count // by calling updateRemaining(). --mRemaining; } // Is this the last of the repliers? if (mRemaining == 0) { // Then this request is finished BUT DO // NOT CLOSE THIS FEED. // // Why? // Because the requestor may be using the // request's feed identifier to track the // request. Once closed, the feed // identifier will be recycled. // // Why is this a problem? The request // feed identifier is immutable. // // Consider the following scenario. This // now defunct request is feed ID 1. We // close the request and toss ID 1 back // into the ID pool for re-use. Then we // asynchronously post this reply to the // requestor. While the reply is en // route, the requestor creates a new // request AND IT IS ASSIGNED FEED ID 1. // Now when the reply for the first // feed ID 1 arrives, the requestor will // think it is for the second feed ID 1. // // Solution: // Have ReplyTask.run() check the request // start *after* delivering the reply. // If the request is done or canceled, // then close the request and recycle the // feed identifier. If the requestor // hangs on to a feed identifier after // receiving the final reply, then that // is an application bug. setState(RequestState.DONE); } } // Foward the response to the requesting client. // Note: since replies come from at various times, // the caller has *not* acquired the dispatch lock // for this method. mEClient.dispatch( new ReplyTask( mRemaining, msg, this, findCallback(mc))); ESubject.exhaust(msg); } } // end of reply(int, EReplyMessage, ERequest) /** * Update the remaining replier count. * @param previous previous remaining replier count. * @param next next remaining replier count. */ /* package */ void updateRemaining(final int previous, final int next) { mRemaining += (next - previous); sLogger.debug( "{}: {} remaining (active={}, state={})", this, mRemaining, mIsActive.get(), mRequestState); } // end of updateRemaining(int, int) /** * Sets the next request state, logging the state change. * If the state is either {@link RequestState#DONE} or * {@link RequestState#CANCELED}, then the request * @param nextState next request state. */ private void setState(final RequestState nextState) { sLogger.trace("{}: {} -> {}.", this, mRequestState, nextState); mRequestState = nextState; } // end of setState(RequestState) /** * Returns the reply callback associated with the given * message class. * @param mc find reply callback for this message class. * @return reply callback. */ private ReplyCallback findCallback(final Class mc) { ReplyCallback retval = null; // Is this message class already in the reply // callback map? if (mReplyCallbacks.containsKey(mc)) { // Yes. Use that entry. retval = mReplyCallbacks.get(mc); } // No. Look for the reply callback entry closest to // the target message class. else { Class bc = null; int distance = Integer.MAX_VALUE; int cd; // Look for reply callback matching the message // class and within the eBus distance. for (Class key : mReplyCallbacks.keySet()) { cd = subclassDistance(mc, key); if (cd >= 0 && cd < distance) { distance = cd; bc = key; } } if (bc != null) { retval = mReplyCallbacks.get(bc); mReplyCallbacks.put(bc, retval); } } return (retval); } // end of findCallback(Class) } // end of class ERequest /** * This task calls back * {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}. */ private static final class FeedStatusTask extends AbstractClientTask { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * The replier new feed status. */ private final EFeedState mFeedState; /** * Pass the feed state to this method. */ private final FeedStatusCallback mCallback; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new feed status task for the given callback * parameters. * @param feedState {@code true} if the feed is up and * {@code false} if down. * @param feed the feed state applies to this request * feed. * @param cb callback method. */ private FeedStatusTask(final EFeedState feedState, final IERequestFeed feed, final FeedStatusCallback cb) { super (feed); mFeedState = feedState; mCallback = cb; } // end of FeedStatusTask(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Runnable Interface Implementations. // /** * Issues the * {@link ESubscriber#feedStatus(EFeedState, ESubscribeFeed)} * callback, logging any client-thrown exception. */ @Override @SuppressWarnings ("unchecked") public void run() { final Object target = (mFeed.eClient()).target(); sLogger.trace("{} executing", this); if (target != null) { try { mCallback.call( mFeedState, (IERequestFeed) mFeed); } catch (Throwable tex) { final String reason = "{} exception"; final String className = (target.getClass()).getName(); if (sLogger.isDebugEnabled()) { sLogger.warn(reason, className, tex); } else { sLogger.warn(reason, className); } } } } // end of run() // // end of Runnable Interface Implementations. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return ( String.format("FeedStatusTask [feed=%s, state=%s]", mFeed, mFeedState)); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- } // end of FeedStatusTask /** * This task is used to forward an eBus reply message to * {@link ERequestor#reply(int, EReplyMessage, ERequestFeed.ERequest)}. */ private static final class ReplyTask extends AbstractClientTask { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * The number of outstanding repliers. */ private final int mRemaining; /** * Deliver this message to the requestor. */ private final EReplyMessage mMessage; /** * The reply is for this request. */ private final ERequestFeed.ERequest mRequest; /** * Pass the reply message back to the application via * this callback. */ private final ReplyCallback mCallback; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new reply task for the given reply message * and requestor. * @param remaining number of repliers yet to finish * replying. * @param msg the reply message. * @param request the reply is for this request. * @param cb reply callback method. */ private ReplyTask(final int remaining, final EReplyMessage msg, final ERequestFeed.ERequest request, final ReplyCallback cb) { super (request); mRemaining = remaining; mMessage = msg; mRequest = request; mCallback = cb; } // end of ReplyTask(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Runnable Interface Implementations. // /** * Passes the arguments to the client's reply method. * Catches any client-thrown exception and logs it. */ @Override @SuppressWarnings ("unchecked") public void run() { final Object target = (mFeed.eClient()).target(); sLogger.trace("{}: running.", this); if (target != null) { try { mCallback.call( mRemaining, mMessage, mRequest); } catch (Throwable tex) { final String reason = "ReplyTask[{}, {}] exception"; final String className = (target.getClass()).getName(); final EMessageKey key = mMessage.key(); if (sLogger.isDebugEnabled()) { sLogger.warn(reason, className, key, tex); } else { sLogger.warn(reason, className, key); } } } // Is the request dead? if (mRequest.requestState() == RequestState.DONE || mRequest.requestState() == RequestState.CANCELED) { // Yes. NOW it is safe to close the feed now that // the reply is safely delivered. mRequest.close(); } } // end of run() // // end of Runnable Interface Implementations. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return ( String.format( "ReplyTask[remaining=%d, key=%s]", mRemaining, mMessage.key())); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- } // end of class ReplyTask } // end of class ERequestFeed




© 2015 - 2024 Weber Informatics LLC | Privacy Policy