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

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

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

package net.sf.eBus.client;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.ERequestFeed.RequestState;
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.Validator;

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

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

*

* Step 2: * {@link #open(EReplier, EMessageKey, EFeed.FeedScope, ECondition) Open} * a reply feed for a given {@code EReplier} instance and * {@link EMessageKey type+topic message key}. The condition is * optional and may be {@code null}. If provided, then only * request messages satisfying the condition are forwarded to the * replier. *

*

* Use * {@link Builder#cancelRequestCallback(CancelRequestCallback)} * and * {@link Builder#requestCallback(RequestCallback)} to set Java * lambda expressions used in place of {@link EReplier} interface * methods. *

* Step 3: {@link #advertise() Advertise} this * replier to eBus. This allows eBus to match repliers with * requestors. *

*

* Step 4: Wait for * {@link EReplier#request(EReplyFeed.ERequest) requests} * to arrive. Keep the given {@link ERequest} instance handy * because {@code ERequest} is used to post reply messages back * to the requestor, not {@code EReplyFeed}. *

*

* Step 5: * {@link EReplyFeed.ERequest#reply(EReplyMessage) Send} one or * more {@link EReplyMessage reply} messages back to the * requestor. *

*

* Step 6: When the replier is shutting down, * {@link #unadvertise() retract} the reply advertisement and * {@link #close() close} the feed. *

*

Example use of EReplyFeed

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

Step 1: Implement EReplier interface.
public class CatalogReplier implements EReplier {
    private final EMessageKey mKey;
    private final FeedScope mScope;
    private EReplyFeed mFeed;
    private final List<EReplyFeed.ERequest> mRequests;

    public CatalogReplier(final String subject, final FeedScope scope) {
        mKey = new EMessageKey(com.acme.CatalogOrder.class, subject);
        mScope = scope;
        mFeed = null;
        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 = (EReplyFeed.builder()).target(this)
                                          .messageKey(mKey)
                                          .scope(mScope)
                                          .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())
new OrderReply(status, reason));

            // 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 ERequestFeed * * @author Charles Rapp */ public final class EReplyFeed extends ESingleFeed implements IEReplyFeed { //--------------------------------------------------------------- // Inner classes. // /** * {@code EReplyFeed.Builder} is now the preferred * mechanism for creating a {@code EReplyFeed} instance. * A {@code Builder} instance is acquired from * {@link EReplyFeed#builder()}. The following example shows * how to create a {@code EReplyFeed} instance using a * {@code Builder}. The code shows how {@code EReplier} * interface method * {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)} * and * {@link EReplier#request(EReplyFeed.ERequest)} methods may * be replaced with lambda expressions. *
@Override public void startup() {
    final EMessageKey key = new EMessageKey(com.acme.CatalogOrder.class, subject);
    final EReplyFeed feed = (EReplyFeed.builder()).target(this)
                                                  .messageKey(key)
                                                  .scope(EFeed.FeedScope.REMOTE_ONLY)
                                                  .condition(m -> ((CatalogOrder) m).category == Category.APPLIANCES)
                                                  .cancelRequest(this::cancelOrder)
                                                  .requestCallback(this::newOrder)
                                                  .build();
    ...
}
* * @see #builder() * @see #open(EReplier, EMessageKey, EFeed.FeedScope, ECondition) */ public static final class Builder extends ESingleFeed.Builder { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Use this condition to check if the request message * should be forwarded to replier. Default setting is * {@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. // /** * Private method since a {@code Builder} instance may * only be instantiated by {@link #builder()}. * * @see #builder() */ private Builder() { super (FeedType.REPLY_FEED, EReplyFeed.class); mCondition = NO_CONDITION; } // 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() /** * Checks that both request and cancel request callbacks * are defined. * @param problems place invalid configuration settings * in this problems list. * @return {@code problems} to allow for method chaining. */ @Override protected Validator validate(final Validator problems) { // 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) .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)) .requireTrue((mRequestCallback != null || isOverridden(REQUEST_METHOD, EReplyFeed.ERequest.class)), REQUEST_METHOD, REQUEST_METHOD + " not overridden and requestCallback not set") .requireTrue((mCancelCallback != null || isOverridden(CANCEL_METHOD, EReplyFeed.ERequest.class, boolean.class)), "cancelRequestCallback", CANCEL_METHOD + " not overridden and cancelRequestCallback not set")); } // end of validate(Validator) /** * Returns a new {@code EReplyFeed} instance based on * {@code this Builder}'s configuration. * @return new reply feed. */ @Override protected EReplyFeed buildImpl() { return (new EReplyFeed(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. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns message type associated with the configured * message key. * @return request message type. */ private MessageType getMessageType() { return ( (MessageType) DataType.findType( mKey.messageClass())); } // end of getMessageType() /** * Returns request callback method. * @return request callback method. */ private RequestCallback getReplyCallback() { // Was reequest callback explicitly defined? if (mRequestCallback == null) { // No. Create a callback to EReplier.request // method. mRequestCallback = ((EReplier) mTarget)::request; } return (mRequestCallback); } // end of getReplyCallback() /** * Returns cancel request callback method. * @return cancel request callback method. */ private CancelRequestCallback getCancelRequestCallback() { // Was a cancel request callback explicitly defined. if (mCancelCallback == null) { // No. Create a callback to // EReplier.cancelRequest method. mCancelCallback = ((EReplier) mTarget)::cancelRequest; } return (mCancelCallback); } // end of getCancelRequestCallback() // // end of Get Methods. //------------------------------------------------------- //------------------------------------------------------- // 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 /** * {@code ERequest} is responsible for matching an * {@link ERequestFeed.ERequest} with a single * {@link EReplyFeed}. The replier uses * {@link ERequest#reply(EReplyMessage)} to post a * {@link EReplyMessage reply message} back to the requesting * client. Replies cannot be sent using {@link EReplyFeed} * nor can {@code ERequest} instances be retrieved from * {@code EReplyFeed}. It is the application's responsibility * to track active {@code ERequest} instances in order to * send reply messages back to requestors. *

* ({@code EReplyFeed} does track the active {@code ERequest} * instances but for internal purposes only. The reason is * due to {@code ERequest} not having the original request * message. Without the request message, an {@code ERequest} * object is unusable by the application.) *

*

* A {@link ERequestor request client} does not directly * interact with {@code ERequest} instances but with its * {@link ERequestFeed.ERequest}. This is because there is a * one-to-one * relationship between {@code ERequestFeed.ERequest} and * {@code EReplyFeed.ERequest}. *

*

* Each request is given an identifier that is unique for the * request lifespan. Once the request reaches the * {@link RequestState#DONE done} or * {@link RequestState#CANCELED canceled} state, this * identifier is returned an identifier pool for re-use. It * is likely that a later request will be assigned the same * identifier. *

* * @see ERequestFeed * @see ERequestFeed.ERequest * @see EReplyFeed * @see ERequestMessage * @see EReplyMessage */ public static final class ERequest extends ESingleFeed { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Post reply messages and cancel replies to this request * feed. The requestor tracks the request state. */ private final ERequestFeed.ERequest mRequestor; /** * Mark replies as coming from this feed. The request * feed needs to know from where replies originate. */ private final EReplyFeed mReplier; /** * The request message itself. */ private final ERequestMessage mRequest; /** * Contains the functional interface callback for request * messages. If not explicitly set by client, then * defaults to * {@link EReplier#cancelRequest(EReplyFeed.ERequest)}. */ private final CancelRequestCallback mCancelCallback; /** * Tracks whether this request for its replier is active, * cancel in-progress, or done. Initialize to active. */ private RequestState mRequestState; /** * Tracks the number of remaining repliers for this request. * For a local replier, this will always be one. For a remote * replier, this may be > one. */ private int mRemaining; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new request instance which links a reply and * a request feed. * @param reqFeed forward replies to this requestor. * @param repFeed replies come from this feed. * @param request the request message. * @param cb request cancellation callback. */ /* package */ ERequest(final ERequestFeed.ERequest reqFeed, final EReplyFeed repFeed, final ERequestMessage request, final CancelRequestCallback cb) { super (repFeed.mEClient, repFeed.mScope, repFeed.mFeedType, repFeed.mSubject); mRequestor = reqFeed; mReplier = repFeed; mRequest = request; mCancelCallback = cb; mRequestState = RequestState.ACTIVE; // Assume local until updated. mRemaining = 1; } // end of ERequest(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // EFeed Abstract Method Overrides. // /** * Cancels this request if still active. */ @Override protected void inactivate() { if (mRequestState == RequestState.ACTIVE) { state(RequestState.CANCELED); // Post this request cancellation to the replier. (mReplier.mEClient).dispatch( new CancelRequestTask( this, false, mReplier, mCancelCallback)); } } // end of inactivate() @Override /* package */ int updateActivation(final ClientLocation loc, final EFeedState fs) { return (0); } // end of updateActivation(ClientLocation, EFeedState) // // end of Abstract Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return ((mRequest.key()).toString()); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Get Methods. // /** * Returns the associated request message. * @return request message. */ public ERequestMessage request() { return (mRequest); } // end of request() /** * Returns the current request state. * @return the request state. */ public RequestState state() { return (mRequestState); } // end of state() /** * Returns the associated reply feed. * @return a reply feed. */ public EReplyFeed replier() { return (mReplier); } // end of replier() /** * Returns the associated request feed. * @return request feed. */ /* package */ ERequestFeed.ERequest requestor() { return (mRequestor); } // end of requestor() /** * Returns the remote replier count for this request. * @return number of remaining remote repliers. */ /* package */ int remaining() { return (mRemaining); } // end of remaining() // // end of Get Methods. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // /** * Sets the request state to the given value. * @param state the latest request state. */ /* package */ void state(final RequestState state) { if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("Reply %d.%d: setting state to %s.", mReplier.feedId(), mFeedId, state)); } mRequestState = state; // If the new state means that the request is // completed (whether successfully or not), remove // this request from the reply feed and close this // feed. if (state == RequestState.DONE || state == RequestState.CANCELED) { mReplier.requestDone(this); close(); } } // end of state(RequestState) /** * Sets the number of remaining remote repliers for this * request. * @param n remote replier count. */ /* package */ void remoteRemaining(final int n) { if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("Reply %d.%d: number of %s repliers remaining is %,d.", mReplier.feedId(), mFeedId, mReplier.location(), n)); } // Have the request feed update its remaining count. mRequestor.updateRemaining(mRemaining, n); mRemaining = n; } // end of remoteRemaining(int) // // end of Set Methods. //------------------------------------------------------- //------------------------------------------------------- // Local Client API. // Applications should use the following methods to send // a request or a reply to a request. // /** * Forwards the reply message to the request feed. If * this is a final reply (whether OK or error), the * request state is set to done. * @param msg the reply message. * @throws IllegalArgumentException * if {@code msg} is {@code null} or is not a valid reply * message class for this request. * @throws IllegalStateException * if the request state is {@link RequestState#DONE done}. */ public void reply(final EReplyMessage msg) { // Is there a message to send? if (msg == null) { throw ( new IllegalArgumentException("msg is null")); } // Is this reply message valid for the given request? if (!(mReplier.isValidReply(msg.key()))) { // No, wrong message type. throw ( new IllegalArgumentException( (msg.key()).className() + " is not a valid reply message class for " + (mRequest.key()).className())); } if (!(((msg.key()).subject()).equals( (mSubject.key()).subject()))) { throw ( new IllegalArgumentException( "reply subject \"" + (msg.key()).subject() + "\" does not match request message subject \"" + (mSubject.key()).subject() + "\"")); } // Is this request still in-progress? if (mRequestState == RequestState.DONE) { throw ( new IllegalStateException( "request is done")); } // Everything checks out. Is this request finished? if (msg.isFinal() && (mRemaining - 1) == 0) { // Yes. Mark that done. --mRemaining; state(RequestState.DONE); } if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format("Reply %d.%d: forwarding reply message:%n%s", mReplier.feedId(), mFeedId, msg)); } else if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("Reply %d.%d: forwarding reply message %s.", mReplier.feedId(), mFeedId, msg.key())); } // Now hand this reply off to the originating request // feed and that feed will forward it to the client. mRequestor.reply(mRemaining, msg, this); } // end of reply(EReplyMessage) /** * Posts an respond-able cancel request to replier. * @return {@code true} if the request is still active * with this replier and {@code false} if not. */ /* package */ boolean cancel() { final boolean retcode = (mRequestState == RequestState.ACTIVE); if (retcode) { // Post this request cancellation to the replier. (mReplier.mEClient).dispatch( new CancelRequestTask( this, true, mReplier, mCancelCallback)); } return (retcode); } // end of cancel() /** * Forwards a remote reply message to the request feed. * If there are no more remote replies, the request state * is set to done. * @param msg the reply message. */ /* package */ void remoteReply(final EReplyMessage msg) { // Did the remote eBus previously report no more // replies? if (mRemaining == 0) { state(RequestState.DONE); } if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest( String.format("Reply %d.%d: forwarding reply message:%n%s", mReplier.feedId(), mFeedId, msg)); } else if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("Reply %d.%d: forwarding %s.", mReplier.feedId(), mFeedId, msg.key())); } // Now hand this reply off to the originating request // feed and that feed will forward it to the client. mRequestor.reply(mRemaining, msg, this); } // end of remoteReply(EReplyMessage) // // end of Local Client API. //------------------------------------------------------- } // end of class ERequest /** * This task forwards an eBus request to * {@link EReplier#request(EReplyFeed.ERequest)}. */ private static final class RequestTask extends AbstractClientTask { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Pass the request to the application via this callback. */ private final RequestCallback mCallback; //--------------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new request task for the given message and * feed. * @param request the eBus request instance. Post replies * to {@link ERequest#reply(EReplyMessage)}. * @param cb method callback. */ private RequestTask(final EReplyFeed.ERequest request, final RequestCallback cb) { super (request); mCallback = cb; } // end of RequestTask(ERequest, RequestCallback) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Runnable Interface Implementations. // /** * Passes the arguments to * {@link EReplier#request(EReplyFeed.ERequest)}, logging * any client-thrown exception. */ @Override public void run() { final Object target = (mFeed.eClient()).target(); if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest(this.toString()); } if (target != null) { try { mCallback.call((EReplyFeed.ERequest) mFeed); } catch (Throwable tex) { final String reason = String.format( "RequestTask[%s, %s] exception", (target.getClass()).getName(), mFeed); if (sLogger.isLoggable(Level.FINE)) { sLogger.log(Level.WARNING, reason, tex); } else { sLogger.log(Level.WARNING, reason); } } } } // end of run() // // end of Runnable Interface Implementations. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return (String.format("RequestTask[%s]", mFeed)); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- } // end of class RequestTask /** * This task forwards a cancel request to * {@link EReplier#cancelRequest(net.sf.eBus.client.EReplyFeed.ERequest, boolean)}. */ private static final class CancelRequestTask extends AbstractClientTask { //----------------------------------------------------------- // Member data. // //------------------------------------------------------- // Locals. // /** * Canceling this request. */ private final EReplyFeed.ERequest mRequest; /** * Set to {@code true} if replier may respond to this * cancel request and {@code false} if this is a * unilateral request cancellation. */ private final boolean mMayRespond; /** * Pass the request cancellation via this callback. */ private final CancelRequestCallback mCallback; //----------------------------------------------------------- // Member methods. // //------------------------------------------------------- // Constructors. // /** * Creates a new CancelRequestTask instance for the given * request identifier. * @param request canceling this request. * @param respondFlag {@code true} if replier may respond * to this request cancellation. * @param feed the canceled request applies to this feed. * @param cb the method callback. */ private CancelRequestTask(final EReplyFeed.ERequest request, final boolean respondFlag, final EReplyFeed feed, final CancelRequestCallback cb) { super (feed); mRequest = request; mMayRespond = respondFlag; mCallback = cb; } // end of CancelRequestTask(...) // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Runnable Interface Implementations. // /** * Passes the arguments to the * {@link EReplier#cancelRequest(EReplyFeed.ERequest)}, * logging any client-thrown exception. */ @SuppressWarnings({"java:S1181"}) @Override public void run() { final Object target = (mFeed.eClient()).target(); if (sLogger.isLoggable(Level.FINEST)) { sLogger.finest(this.toString()); } if (target != null) { try { mCallback.call(mRequest, mMayRespond); } catch (Throwable tex) { final String reason = String.format( "EReplier.cancelRequest(%s) exception", mFeed); if (sLogger.isLoggable(Level.FINE)) { sLogger.log(Level.WARNING, reason, tex); } else { sLogger.log(Level.WARNING, reason); } } } } // end of run() // // end of Runnable Interface Implementations. //------------------------------------------------------- //------------------------------------------------------- // Object Method Overrides. // @Override public String toString() { return ("CancelRequestTask"); } // end of toString() // // end of Object Method Overrides. //------------------------------------------------------- } // end of class CancelRequestTask //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Constants. // /** * {@link EReplier#request(EReplyFeed.ERequest)} method name. */ /* package */ static final String REQUEST_METHOD = "request"; /** * {@link EReplier#cancelRequest(EReplyFeed.ERequest, boolean)} * method name. */ /* package */ static final String CANCEL_METHOD = "cancelRequest"; //----------------------------------------------------------- // Statics. // /** * Logging subsystem interface. */ private static final Logger sLogger = Logger.getLogger(EReplyFeed.class.getName()); //----------------------------------------------------------- // Locals. // /** * Use this condition to check if the request message should * be forwarded to subscriber. */ private final ECondition mCondition; /** * The active requests accepted by this reply feed. */ private final List mRequests; /** * The request message data type. Used to determine if reply * messages belong to the * {@link net.sf.eBus.messages.EReplyInfo} allowed message * types. */ private final MessageType mDataType; /** * Contains the functional interface callback for request * messages. If not explicitly set by client, then defaults * to * {@link EReplier#request(EReplyFeed.ERequest)}. */ // TODO: Make final when open methods are removed. 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)}. */ // TODO: Make final when open methods are removed. private CancelRequestCallback mCancelCallback; //--------------------------------------------------------------- // Member methods. // //----------------------------------------------------------- // Constructors. // /** * Creates a new reply feed for the given client and request * subject. * @param client the application client. * @param scope whether this feed support local feeds, remote * feeds, or both. * @param condition the replier accepts requests only if it * passes this condition. * @param dataType the request message type. Used to * determine if the reply message is in the request message * {@link net.sf.eBus.messages.EReplyInfo} list. * @param subject this feed is associated with this request * subject. */ @Deprecated private EReplyFeed(final EClient client, final FeedScope scope, final ECondition condition, final MessageType dataType, final ERequestSubject subject) { super (client, scope, FeedType.REPLY_FEED, subject); mCondition = condition; mRequests = new ArrayList<>(); mDataType = dataType; mRequestCallback = null; mCancelCallback = null; } // end of EReplyFeed(...) /** * Creates a new reply feed based on the builder * configuration. Note that the builder configuration is * guaranteed to be valid prior to calling this constructor. * @param builder contains valid reply feed configuration. */ private EReplyFeed(final Builder builder) { super (builder); mCondition = builder.mCondition; mRequests = new ArrayList<>(); mDataType = builder.getMessageType(); mRequestCallback = builder.getReplyCallback(); mCancelCallback = builder.getCancelRequestCallback(); } // end of EReplyFeed(Builder) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // EFeed Interface Implementation. // /** * If the advertisement is in place, then un-advertises it, * sends a final error reply to all extant requests, and if * this feed's scope is local & remote or remote only, * removes this feed from the advertisers list. * * @see EFeed#close() */ @Override protected void inactivate() { if (mInPlace) { ((ERequestSubject) mSubject).unadvertise(this); mInPlace = false; // Fail all extant requests since this replier is no // longer in business. failRequests("replier closed"); } } // end of inactivate() /** * Always returns zero because reply feeds do no have the * concept of activation. * @param loc contra-feed location. * @param fs contra-feed state. * @return zero. */ @Override /* package */ int updateActivation(final ClientLocation loc, final EFeedState fs) { return (0); } // end of updateActivation(ClientLocation, EFeedState) // // end of EFeed Interface Implementation. //----------------------------------------------------------- //----------------------------------------------------------- // Get Methods. // /** * 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() /** * Returns {@code true} if {@code repKey} is an allowed * reply message for this request and {@code false} if * not. A reply which is not allowed for the request will not * be sent to the requester. * @param repKey the reply message key. * @return {@code true} if the reply message key is allowed * for this request. */ /* package */ boolean isValidReply(final EMessageKey repKey) { return (mDataType.isValidReply(repKey.messageClass())); } // end of isValidReply(EMessageKey) // // end of Get Methods. //----------------------------------------------------------- //----------------------------------------------------------- // Set Methods. // /** * 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. * @param cb the request callback. May be {@code null}. * @throws IllegalStateException * if this feed is either closed or advertised. */ @Override public void requestCallback(final RequestCallback cb) { if (!mIsActive.get()) { throw ( new IllegalStateException("feed is inactive")); } if (mInPlace) { throw ( new IllegalStateException( "advertisement in place")); } mRequestCallback = cb; } // 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. * @param cb the cancel request callback. May be * {@code null}. * @throws IllegalStateException * if this feed is either closed or advertised. */ @Override public void cancelRequestCallback(final CancelRequestCallback cb) { if (!mIsActive.get()) { throw ( new IllegalStateException("feed is inactive")); } if (mInPlace) { throw ( new IllegalStateException( "advertisement in place")); } mCancelCallback = cb; } // end of cancelRequestCallback(CancelRequestCallback) /** * Updates the replier feed state to the given value. If * {@code update} equals the currently stored reply feed * state, nothing is done. Otherwise, the updated value is * stored. If this feed is advertised to the server and * the subscription feed is up, then this update is forwarded * to subscribed requestors. * @param update the new reply feed state. * @throws NullPointerException * if {@code update} is {@code null}. * @throws IllegalStateException * if this feed was closed and is inactive or is not * advertised. */ @Override public void updateFeedState(final EFeedState update) { Objects.requireNonNull(update, "update is null"); if (!mIsActive.get()) { throw ( new IllegalStateException("feed is inactive")); } if (!mInPlace) { throw ( new IllegalStateException( "feed not advertised")); } // Does this update actually change anything? if (update != mFeedState) { // Yes. Apply the update. mFeedState = update; if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s replier %d, feed %d: setting %s feed state to %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, key(), update, mScope)); } // Forward the update to the subject. ((ERequestSubject) mSubject).updateFeedState(this); } // No change. Nothing to do. } // end of updateFeedState(EFeedState) /** * Returns {@code true} if the replier is still active and * the request message passes the reply condition. If this * method returns {@code true}, then * {@link #request(ERequestFeed.ERequest, ERequestMessage)} * is used to pass the request to the replier. * @param msg test this request message against the reply * condition. * @param loc requestor location. * @return {@code true} if {@code msg} is acceptable. * * @see #open(EReplier, EMessageKey, EFeed.FeedScope, ECondition) * @see #request(ERequestFeed.ERequest, ERequestMessage) */ /* package */ boolean checkRequest(final ERequestMessage msg, final ClientLocation loc) { return (mIsActive.get() && mCondition.test(msg) && mScope.supports(loc)); } // end of checkRequest(ERequestMessage, ClientLocation) /** * Forwards the given request message and request object to * the client, returning a {@link ERequest} object. Note that * {@link #checkRequest(ERequestMessage, EClient.ClientLocation)} * was previously called to determine if the replier accepts * the request message. * {@link ERequestSubject} can determine if any repliers * accepted the request. If no repliers accepted or * this reply is inactive, then returns {@code null}. * @param request the request associated with the * message. * @param message the request message to be posted to replier * clients. * @return request instance linked to * {@link ERequestFeed.ERequest}. May be {@code null}. * * @see #checkRequest(ERequestMessage, EClient.ClientLocation) */ /* package */ EReplyFeed.ERequest request(final ERequestFeed.ERequest request, final ERequestMessage message) { EReplyFeed.ERequest retval; // Is this feed still alive? // Is this feed up? if (!mIsActive.get() || mFeedState == EFeedState.DOWN) { // No, this feed is gone or down. retval = null; } else { // Yes. Add the request to the active requests list // BUT DO NOT DISPATCH THE REQUEST TO THE REPLIER // CLIENT. // The bookkeeping needed to process the reply is // not yet in place. retval = new EReplyFeed.ERequest(request, this, message, mCancelCallback); synchronized (mRequests) { mRequests.add(retval); } } return (retval); } // end of request(ERequest, ERequestMessage) // // end of Set Methods. //----------------------------------------------------------- /** * Returns a new {@code EReplyFeed} builder instance. This * instance should be used to build a single reply feed * instance and not used to create multiple such feeds. * @return new reply feed builder. */ public static Builder builder() { return (new Builder()); } // end of builder() /** * Returns a reply advertiser feed for the specified request * message class and subject. The calls {@link #advertise} * on the returned feed in order to start receiving the * specified request messages. * @param client the application object subscribing to the * request. May not be {@code null}. * @param key the request message class and subject. * May not be {@code null} and must reference a request * message class. * @param scope whether this feed supports only local * clients, remote clients, or both. * @param condition accept request messages only if the * messages passes this condition. May be {@code null}. If * {@code null}, then the * {@link #NO_CONDITION default condition} which accepts all * request messages is used. * @return the newly created reply feed. * @throws NullPointerException * if any of the required parameters is {@code null}. * @throws IllegalArgumentException * if any of the given parameters is invalid. * @deprecated Please use {@link Builder} to create eBus * feeds. * * @see #advertise() * @see EFeed#close() * @see #builder() */ @SuppressWarnings ("unchecked") @Deprecated public static EReplyFeed open(final EReplier client, final EMessageKey key, final FeedScope scope, ECondition condition) { // Validate the parameters. // Are the parameters non-null references? Objects.requireNonNull(client, "client is null"); Objects.requireNonNull(key, "key is null"); Objects.requireNonNull(scope, "scope is null"); // Is the message key for a request? if (!key.isRequest()) { throw ( new IllegalArgumentException( String.format( "%s is not a request message", key))); } // Are the feed scope and message scope in agreement? checkScopes(key, scope); final MessageType dataType = (MessageType) DataType.findType(key.messageClass()); return (open(client, key, scope, condition, ClientLocation.LOCAL, dataType, false)); } // end of open(...) /** * Returns a reply advertiser feed for the specified request * message class and subject. The calls {@link #advertise} * on the returned feed in order to start receiving the * specified request messages. *

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

* @param client the application object subscribing to the * request. May not be {@code null}. * @param key the request message class and subject. * May not be {@code null} and must reference a request * message class. * @param scope whether this feed supports only local * clients, remote clients, or both. * @param condition accept request messages only if the * messages passes this condition. May be {@code null}. If * {@code null}, then the * {@link #NO_CONDITION default condition} which accepts all * request messages is used. * @param l {@code client} location. * @param dataType the request message type. Used to * determine if the reply message is in the request message * {@link net.sf.eBus.messages.EReplyInfo} list. * @param isMulti {@code true} if this is part of a multiple * key feed. If {@code true}, this feed is not added to the * client feed list. * @return the newly created reply feed. */ /* package */ static EReplyFeed open(final EReplier client, final EMessageKey key, final FeedScope scope, final ECondition condition, final ClientLocation l, final MessageType dataType, final boolean isMulti) { // If a null condition was passed in, then use the // default NO_CONDITION. final ECondition replyCondition = (condition == null ? NO_CONDITION : condition); final EClient eClient = EClient.findOrCreateClient(client, l); final ERequestSubject subject = ERequestSubject.findOrCreate(key); final EReplyFeed retval = new EReplyFeed(eClient, scope, replyCondition, dataType, subject); // Let the client know it is being referenced by another // feed - but only if this is not part of a multiple key // feed. In that case, the multiple key feed is added to // the client. if (!isMulti) { eClient.addFeed(retval); } if (sLogger.isLoggable(Level.FINE)) { sLogger.fine( String.format( "%s replier %d, Feed %d: opened %s (%s).", l, eClient.clientId(), retval.feedId(), key, scope)); } return (retval); } // end of open(...) /** * Advertises this replier fed to the associated request * subject. If this feed is currently advertised to the * subject, then does nothing. * @throws IllegalStateException * if this feed is closed or if the client did not override * {@link EReplier} methods nor put the required callbacks in * place. * * @see #unadvertise() * @see #updateFeedState(EFeedState) * @see EFeed#close() */ @Override public void advertise() { if (!mIsActive.get()) { throw ( new IllegalStateException("feed is inactive")); } if (!mInPlace) { // If the callbacks were not put in place, then use // the defaults. if (mRequestCallback == null) { // Did the client override the request method? if (!isOverridden(REQUEST_METHOD, EReplyFeed.ERequest.class)) { // That's a Bozo no-no. throw ( new IllegalStateException( REQUEST_METHOD + " not overridden and requestCallback not set")); } mRequestCallback = ((EReplier) mEClient.target())::request; } if (mCancelCallback == null) { // Did the client override the cancelRequest // method? if (!isOverridden(CANCEL_METHOD, EReplyFeed.ERequest.class, boolean.class)) { // That's a negative. throw ( new IllegalStateException( CANCEL_METHOD + " not overridden and cancelRequestCallback not set")); } mCancelCallback = ((EReplier) mEClient.target())::cancelRequest; } if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s replier %d, Feed %d: advertising %s (%s).", mEClient.location(), mEClient.clientId(), mFeedId, mSubject.key(), mScope)); } ((ERequestSubject) mSubject).advertise(this); mInPlace = true; } } // end of subscribe() /** * Retracts this replier feed from the associated request * subject. Does nothing if this feed is not currently * advertised or closed. * @throws IllegalStateException * if this feed was closed and is inactive. * * @see #advertise() * @see EFeed#close() */ @Override public void unadvertise() { if (!mIsActive.get()) { throw ( new IllegalStateException("feed is inactive")); } if (mInPlace) { if (sLogger.isLoggable(Level.FINER)) { sLogger.finer( String.format("%s replier %d, Feed %d: unadvertising (%s).", mEClient.location(), mEClient.clientId(), mFeedId, mScope)); } ((ERequestSubject) mSubject).unadvertise(this); mInPlace = false; mActivationCount = 0; mFeedState = EFeedState.DOWN; // Fail all extant requests since this replier is no // longer in business. failRequests("replier unadvertised"); } } // end of unsubscribe() /** * Dispatch {@code request} to the replier client. * @param request post this request to the replier. */ /* package */ void dispatch(final EReplyFeed.ERequest request) { mEClient.dispatch( new RequestTask(request, mRequestCallback)); } // end of dispatch(EReplyFeed.ERequest) /** * Removes the completed request from the requests list. * @param request the newly completed request. */ /* package */ void requestDone(final ERequest request) { synchronized (mRequests) { mRequests.remove(request); } } // end of requestDone(ERequest) /** * Posts an error reply containing the given reason to each * outstanding requests and then clears the request list. * @param reason reason why the request is being rejected. */ private void failRequests(final String reason) { final List requests = new ArrayList<>(); // Clear out the now defunct requests and work off of the // copy. synchronized (mRequests) { requests.addAll(mRequests); mRequests.clear(); } requests.forEach( request -> { final EReplyMessage.Builder builder = EReplyMessage.builder(); request.reply(builder.subject((request.key()).subject()) .replyStatus(ReplyStatus.ERROR) .replyReason(reason) .build()); }); } // end of failRequests(String) } // end of class EReplyFeed




© 2015 - 2025 Weber Informatics LLC | Privacy Policy