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

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

The newest version!
//
// Copyright 2017 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.Map;
import java.util.Objects;
import net.sf.eBus.client.ERequestFeed.ERequest;
import static net.sf.eBus.client.ERequestFeed.FEED_STATUS_METHOD;
import static net.sf.eBus.client.ERequestFeed.REPLY_METHOD;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
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;

/**
 * This feed acts as a proxy for handling multiple
 * {@link ERequestFeed}s on behalf of a {@link ERequestor}
 * client. A replier opens a multi-subject reply feed for a
 * specified request message class and zero or more message
 * subjects. These subjects may be specified as a list or a
 * regular expression query. If a query is used, then the message
 * class and subject query are used to search the message
 * dictionary for all matching subjects. Matching subjects are
 * used to create the initial subordinate {@code ERequestFeed}s.
 * 

* The multi-subject request feed coordinates subordinate request * feeds so they are given the same configuration and are in the * same state (open, subscribed, un-subscribed, closed). *

*

* While the multi-subject feed is open, new subordinate request * feeds may be {@link #addFeed(String) added to} or * {@link #closeFeed(String) removed from} the multi-subject feed. * Newly added subordinate feeds are configured and put into the * same state as the existing subordinate feeds. *

*

Example use of EMultiRequestFeed

*
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;
import net.sf.eBus.util.regex.Pattern;

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

    private final FeedScope mScope;
    private final List<ERequestFeed.ERequest> mRequests;
    private EMultiRequestFeed mFeed;

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

    @Override
    public void startup() {
        try {
            Step 2: Open request feed. May override ERequestor interface methods.
            mFeed = (EMultiRequestFeed.builder()).target(this)
                                                 .scope(mScope)
                                                 .messageClass(com.acme.CatalogOrder.class)
                                                 .query(mQuery)
                                                 .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 for given subject.
        } else {
            // Up. There is at least one replier for subject.
        }
    }

    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 EMultiReplyFeed * * @author Charles W. Rapp */ public final class EMultiRequestFeed extends EMultiFeed implements IERequestFeed { //--------------------------------------------------------------- // Member data. // //----------------------------------------------------------- // Locals. // /** * Contains the functional interface callback for feed * status updates. If not explicitly set by client, then * defaults to * {@link ERequestor#feedStatus(EFeedState, ERequestFeed)}. * This callback is applied to all subordinate request feeds. */ 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)}. * This callback is applied to all subordinate request feeds. */ 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 multi-request feed instance based on * validated builder settings. * @param builder contains multi-request feed settings. */ private EMultiRequestFeed(final Builder builder) { super (builder); mStatusCallback = builder.mStatusCallback; mReplyCallbacks = builder.mReplyCallbacks; mCloseFlag = builder.mCloseFlag; } // end of EMultiRequestFeed(Builder) // // end of Constructors. //----------------------------------------------------------- //----------------------------------------------------------- // Abstract Method Implementations. // /** * Returns a newly minted subordinate request feed for the * given key. The returned feed's configuration is the same * as existing subordinate feeds. * @param key create a request feed for this key. * @return a subordinate request feed. */ @Override protected ERequestFeed createFeed(final EMessageKey key) { final ERequestor requester = (ERequestor) mEClient.target(); final ERequestFeed.Builder builder = ERequestFeed.builder(); builder.target(requester) .location(location()) .messageKey(key) .scope(mScope) .mayClose(mCloseFlag) .statusCallback(mStatusCallback); mReplyCallbacks.entrySet() .stream() .forEach( e -> builder.replyCallback( e.getKey(), e.getValue())); return (builder.build()); } // end of createFeed(EMessageKey) /** * Sets the status and reply callbacks as per the multi-subject * configuration and subscribes the subordinate feeds. * @param feed advertise this subordinate request feed. */ @Override protected void putFeedInPlace(final ERequestFeed feed) { // Must set the callbacks (if any) before subscribing. feed.replyCallbacks(mReplyCallbacks); feed.subscribe(); } // end of putFeedInPlace(ERequestFeed) // // end of Abstract Method Implementations. //----------------------------------------------------------- /** * Returns a new {@code EMultiRequestFeed} 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 each subordinate {@link ERequestFeed}. If this * feed is currently subscribed, then does nothing. The * requestor client will receive a * {@link ERequestor#feedStatus(EFeedState, IERequestFeed)} * callback from each subordinate request feed. * @throws IllegalStateException * if this feed is closed or the client did not override * not put in place the required callback methods. * * @see #unsubscribe() * @see #close() */ @Override public void subscribe() { if (!mIsActive.get()) { throw (new IllegalStateException(FEED_IS_INACTIVE)); } // If not already subscribed, then subscribe now. if (!mInPlace) { sLogger.debug( "{} multi-subject requestor {}: subscribing ({}).", mEClient.location(), mEClient.clientId(), mScope); // Subscribe each subordinate feed. mFeeds.values() .stream() .map( feed -> { feed.replyCallbacks(mReplyCallbacks); return (feed); }) .forEachOrdered(ERequestFeed::subscribe); mInPlace = true; // If this multi-subject feed is query based, then // listen for subject updates. if (mQuery != null) { ESubject.addListener(this); } } } // end of subscribe() /** * Retracts this multi-subject request feed by un-subscribing * each subordinate request feed. Does nothing if this feed * is not currently subscribed. * @throws IllegalStateException * if this multi-subject request feed is closed. * * @see #subscribe() * @see #close() */ @Override public void unsubscribe() { if (!mIsActive.get()) { throw (new IllegalStateException(FEED_IS_INACTIVE)); } if (mInPlace) { sLogger.debug( "{} multi-subject requestor {}: unsubscribing ({}).", mEClient.location(), mEClient.clientId(), mScope); // Unadvertise each subordinate feed. mFeeds.values() .stream() .forEachOrdered(ERequestFeed::unsubscribe); // This feed is no longer subscribed. mInPlace = false; // Stop listening for subject updates. ESubject.removeListener(this); } } // end of unsubscribe() /** * Posts a request message to all replier via the subordinate * request feed matching the message's key. * @param msg post this request message to the matching * subordinate feed. * @return the {@link ERequestFeed.ERequest} feed used to * interact with the active request. * @throws NullPointerException * if {@code msg} is {@code null}. * @throws IllegalArgumentException * if {@code msg} message key does not reference a known * subordinate reply feed. * @throws IllegalStateException * if this feed is inactive, not advertised, or there are no * repliers available to respond to the request. */ public ERequest request(final ERequestMessage msg) { // Is the message null? Objects.requireNonNull(msg, "msg is null"); // Is this feed still active? if (!mIsActive.get()) { throw (new IllegalStateException(FEED_IS_INACTIVE)); } // Is the subscription in place? if (!mInPlace) { // No. Gotta do that first. throw ( new IllegalStateException( "feed not subscribed")); } // Does the message reference a known subordinate feed? final String subject = (msg.key()).subject(); if (!mFeeds.containsKey(subject)) { // No. throw ( new IllegalArgumentException( subject + " is an unknown feed")); } // Pass the message to the subordinate request and let // it do the additional checks. return ((mFeeds.get(subject)).doRequest(msg)); } // end of request(ERequestMessage) //--------------------------------------------------------------- // Inner classes. // /** * {@code EMultiRequestFeed.Builder} is the mechanism for * creating an {@code EMultiRequestFeed} instance. A * {@code Builder} instance is acquired from * {@link EMultiRequestFeed#builder()}. The following example * shows how to create an {@code EMultiRequestFeed} instance * using a {@code Builder}. The code assumes that the target * class implements {@code ERequestor} interface methods. *
@Overricde public void startup() {
    final EMultiRequestFeed feed = (EMultiRequestFeed.builder()).target(this)
                                                                .messageClass(com.acme.CatalogOrder.class)
                                                                .scope(EFeed.FeedScope.REMOTE_ONLY)
                                                                .subjects(mSubjectsList) // msSubjectsList is List<String> subject list
                                                                // Call .statusCallback(lambda expression) and
                                                                // .replyCallback(lambda expression) to replace feedStatus, reply methods
                                                                .build();
}
*/ public static final class Builder extends EMultiFeed.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. // /** * Creates a new multi-request feed builder instance. * "May close" flag set to {@code true}. */ private Builder() { super (EMultiRequestFeed.class, ERequestMessage.class); mCloseFlag = true; } // end of Builder() // // end of Constructors. //------------------------------------------------------- //------------------------------------------------------- // Abstract Method Overrides. // @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) .requireNotNull(mStatusCallback, "statusCallback"); checkReplyCallbacks(problems); return (problems); } // end of validate(Validator) @Override protected ERequestFeed createFeed(final EMessageKey key) { final ERequestFeed.Builder builder = ERequestFeed.builder(); builder.target((ERequestor) mTarget) .location(mLocation) .messageKey(key) .scope(mScope) .mayClose(mCloseFlag) .statusCallback(mStatusCallback); mReplyCallbacks.entrySet() .stream() .forEach( e -> builder.replyCallback( e.getKey(), e.getValue())); return (builder.build()); } // end of createFeed(EMessageKey) @Override protected EMultiRequestFeed buildImpl() { return (new EMultiRequestFeed(this)); } // end of buildImpl() @Override protected Builder self() { return (this); } // end of self() // // end of Abstract Method Overrides. //------------------------------------------------------- //------------------------------------------------------- // Set Methods. // @Override public Builder messageClass(final Class mc) { // Throws NullPointerException if mc is null. super.messageClass(mc); // Message class is set. Now fill in the reply // callback map. mReplyCallbacks = ERequestFeed.createReplyCallbacks( (MessageType) DataType.findType(mc)); return (this); } // end of messageClass(final Class mc) /** * 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 #messageClass(java.lang.Class) message class} * not set prior to setting reply callback. * * @see #messageClass(java.lang.Class) * @see #replyCallback(ReplyCallback) */ public Builder replyCallback(final Class mc, final ReplyCallback cb) { // Has the message class been set? if (mMsgClass == null) { throw ( new IllegalStateException( "messageClass must be set adding replyCallback")); } Objects.requireNonNull(mc, "mc is null"); if (!mReplyCallbacks.containsKey(mc)) { throw ( new IllegalArgumentException( mc.getSimpleName() + " is not a 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 #messageClass(java.lang.Class) message class} * was not set prior to setting the reply callback. * * @see #replyCallback(Class, ReplyCallback) */ public Builder replyCallback(final ReplyCallback cb) { // Has the message class been set? if (mMsgClass == null) { throw ( new IllegalStateException( "messageClass 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 } // end of class EMultiRequestFeed




© 2015 - 2024 Weber Informatics LLC | Privacy Policy