
net.sf.eBus.client.EFeed Maven / Gradle / Ivy
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later
// version.
//
// This library is distributed in the hope that it will be
// useful, but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General
// Public License along with this library; if not, write to the
//
// Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330,
// Boston, MA
// 02111-1307 USA
//
// The Initial Developer of the Original Code is Charles W. Rapp.
// Portions created by Charles W. Rapp are
// Copyright 2015, 2016, 2018. Charles W. Rapp
// All Rights Reserved.
//
package net.sf.eBus.client;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.ERequestMessage;
import net.sf.eBus.messages.ESystemMessage;
import net.sf.eBus.util.regex.Pattern;
/**
* Applications interface with eBus through feed instances. An
* application class implements the eBus interface associated
* with the feed (for example, {@link EPublisher} for
* {@link EPublishFeed}), opens a feed instance, and then
* activates the feed. The feed can be activated, deactivated
* multiple times. Once the feed is {@link EFeed#close() closed},
* the feed instance cannot be used again. A new feed instance
* must be opened again.
*
* An application defines a feed's {@link FeedScope scope} when
* opening the feed. This scope defines the feed's visibility.
* That feed may be visible only within the local JVM, within
* both local and remote JVMs, and in remote JVMs only. For
* example, if a {@link ESubscribeFeed subscription feed} has
* local-only scope, then it will receive notifications from
* local {@link EPublishFeed publisher feeds} only. Notifications
* from remote publishers will not be forwarded to the local-only
* subscriber. The following table shows the interface between
* feed scopes:
*
*
* Feed Scope
*
*
* Local Only
* Local & Remote
* Remote Only
*
*
* Local Only
* Match
* Match
* No match
*
*
* Local & Remote
* Match
* Match
* Match
*
*
* Remote Only
* No match
* Match
* No match
*
*
*
* (Notice that a remote feed may only support a local &
* remote feed and not other remote only feeds.)
*
*
* Feed scope gives the application developer control over how
* "far" a message will go. If a notification message is intended
* to stay within a JVM, both the publish and subscribe feeds may
* be set to a local only scope. If a notification is meant for
* remote access only, the the publish feed is set to remote only
* scope. These examples also apply to request/reply feeds.
*
*
* eBus calls an application instance back from only one thread
* at any given time. While that thread may change over time,
* only one eBus thread will callback the application instance
* at any given time. The eBus API is thread-safe and an
* application may call eBus methods from multiple, simultaneous
* threads. The benefit of this model is that if an application
* instance is accessed only by eBus only and not by any other
* non-eBus thread, then the application instance is effectively
* single-threaded. Such an application instance does not need to
* use {@code synchronized} keyword or locks, simplifying the
* application class implementation.
*
*
* A feed maintains a {@link EClient weak reference} to the
* application instance. If the application is finalized while
* still owning open feeds, those feeds will be automatically
* closed when eBus detects this finalization.
*
*
* Each feed is assigned a identifier that is unique within the
* application instance scope and feed lifespan. Once a feed is
* closed, its identifier is recycled. Put in another way: an
* application instance's active feeds all have unique integer
* identifiers. These identifiers are not unique across
* different application instances. These identifiers are
* not unique across an application instance lifespan if
* that instance opens and closes feeds multiple times. It is
* likely that newly opened feed will have the same identifier as
* a previously closed feed.
*
*
* The eBus API is intended to be extended with new feed
* subclasses. These extensions would provide more sophisticated
* notification and request/reply types. One example is a
* notification feed that combines historical and live updates or
* just historical, depending on what the subscriber requests.
* This allows a subscriber to join the feed at any time, not
* missing any previously posted notifications.
*
* Message Key Dictionary
*
* eBus v. 4.5.0 added the ability to directly add messages keys
* to the eBus message key dictionary and retrieve keys from said
* dictionary. Prior to this version, message keys were
* indirectly added to the dictionary when opening feeds. This
* feature added to support the new multikey feeds
* {@link EMultiPublishFeed}, {@link EMultiSubscribeFeed},
* and {@link EMultiReplyFeed}. Multi-key feeds may use a
* {@link net.sf.eBus.util.regex.Pattern query} to match a
* variable number of keys. This is why
* {@link #addKey(EMessageKey)},
* {@link #addAllKeys(java.util.Collection)} and
* {@link #loadKeys(ObjectInputStream)} methods are provided:
* unless the message key dictionary is populated with keys prior
* to creating a multi-key query feed, the query would find no
* matching keys.
*
*
* Multi-key feeds act as proxies between the application
* client and the individual {@code EPublishFeed},
* {@code ESubscribeFeed}, {@code ERequestFeed} and
* {@code EReplyFeed} in the multi-key feed. The multi-key feed
* opens, advertises/subscribes, and closes all the subordinate
* feeds in unison. The individual feeds all reference the same
* client and client callback methods. If a multi-key feed is for
* 100 keys, then the client receives callbacks from all 100
* subordinate feed and not for the single multi-key
* feed.
*
*
* Note: a multi-key feed is not {@code EFeed}
* subclass. However, multi-key feed behavior is the same as an
* {@code EFeed} and may be treated by the application as if
* it were an {@code EFeed}.
*
*
* @see ESubject
* @see ENotifyFeed
* @see EPublishFeed
* @see ESubscribeFeed
* @see ERequestFeed
* @see EReplyFeed
* @see EMultiPublishFeed
* @see EMultiSubscribeFeed
* @see EMultiRequestFeed
* @see EMultiReplyFeed
*
* @author Charles W. Rapp
*/
public abstract class EFeed
implements IEFeed
{
//---------------------------------------------------------------
// Inner classes.
//
/**
* Base class for eBus client callback tasks created by
* feeds. Contains the eBus feed object.
*/
protected static abstract class AbstractClientTask
implements Runnable
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* The callback is from this message feed instance.
*/
protected final IEFeed mFeed;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a client callback task for this eBus client
* and message feed.
* @param feed the callback is from this message feed
* instance.
*/
protected AbstractClientTask(final IEFeed feed)
{
mFeed = feed;
} // end of AbstractClientTask(IEFeed)
//
// end of Constructors.
//-------------------------------------------------------
} // end of class AbstractClientTask
/**
* Used to issue a feed status callback.
* @param feed status applies to this feed type.
*/
protected final class StatusTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* The publisher 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 feed.
* @param cb post feed state update to this callback.
*/
public StatusTask(final EFeedState feedState,
final EFeed feed,
final FeedStatusCallback cb)
{
super (feed);
mFeedState = feedState;
mCallback = cb;
} // end of StatusTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementation.
//
/**
* Issues the feed status callback, logging any
* client-thrown exception.
*/
@Override
@SuppressWarnings ("unchecked")
public void run()
{
final Object target = mEClient.target();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(this.toString());
}
if (target != null)
{
try
{
mCallback.call(mFeedState, (T) mFeed);
}
catch (Throwable tex)
{
final String reason =
String.format(
"%s publish status callback exception",
(target.getClass()).getName());
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(Level.WARNING, reason, tex);
}
else
{
sLogger.log(Level.WARNING, reason);
}
}
}
return;
} // end of run()
//
// end of Runnable Interface Implementation.
//-------------------------------------------------------
} // end of StatusTask
/**
* This task forwards a notification message to
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed)}.
*/
protected final class NotifyTask
extends AbstractClientTask
{
//-----------------------------------------------------------
// Member data.
//
/**
* Forward this message to the subscriber if the message
* satisfies the subscription condition.
*/
private final ENotificationMessage mMessage;
/**
* Apply {@link #mMessage} to this condition and deliver
* message if-and-only-if the message satisfies the
* condition.
*/
private final ECondition mCondition;
/**
* Forward notification message to this callback.
*/
private final NotifyCallback mCallback;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a notify callback task for the given
* parameters.
* @param message the notification message.
* @param condition subscription condition.
* @param feed the message is from this notification
* feed.
* @param cb notification message callback.
*/
public NotifyTask(final ENotificationMessage message,
final ECondition condition,
final IESubscribeFeed feed,
final NotifyCallback cb)
{
super (feed);
mMessage = message;
mCondition = condition;
mCallback = cb;
} // end of NotifyTask(...)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementations.
//
/**
* Issues the
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed)}
* callback if the message satisfies the
* subscription condition. Logs any client-thrown
* exception.
*/
@Override
public void run()
{
final Object target = mEClient.target();
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(this.toString());
}
if (target != null)
{
try
{
// Is the subscriber interested in this
// message?
if (mCondition.test(mMessage))
{
// Yes. Forward the notification message
// to the subscriber.
mCallback.call(
mMessage, (IESubscribeFeed) mFeed);
}
}
catch (Throwable tex)
{
final String reason =
String.format(
"NotifyTask[%s, %s] exception",
(target.getClass()).getName(),
mMessage.key());
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(Level.WARNING, reason, tex);
}
else
{
sLogger.log(Level.WARNING, reason);
}
}
}
return;
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (
String.format(
"NotifyTask[key=%s]", mMessage.key()));
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // end of class NotifyTask
//---------------------------------------------------------------
// Enums.
//
/**
* Feed scope is either restricted to this JVM, to both this
* JVM and remote JVMs, and to remote JVMs only. The feed
* scope is used to determine if two opposing feeds
* ({@link EPublishFeed publish} and
* {@link ESubscribeFeed subscibe} or
* {@link EReplyFeed reply} and {@link ERequestFeed}) match
* and so may exchange messages.
*/
public enum FeedScope
{
/**
* This feed is matched only by other feeds within this
* JVM.
*/
LOCAL_ONLY (ClientLocation.LOCAL.mask,
new ClientLocation[] {ClientLocation.LOCAL},
"local only"),
/**
* This feed is matched by both local and remote feeds.
*/
LOCAL_AND_REMOTE ((ClientLocation.LOCAL.mask |
ClientLocation.REMOTE.mask),
new ClientLocation[]
{
ClientLocation.LOCAL,
ClientLocation.REMOTE
},
"local & remote"),
/**
* This feed is matched only by feeds on remote JVMs.
*/
REMOTE_ONLY (ClientLocation.REMOTE.mask,
new ClientLocation[] {ClientLocation.REMOTE},
"remote only");
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a feed scope with the given description.
* @param locationMask bit mask specifying which
* locations are supported by this scope.
* @param locations the client locations associated with
* this feed scope.
* @param text text describing this feed scope.
*/
private FeedScope(final int locationMask,
final ClientLocation[] locations,
final String text)
{
_locationMask = locationMask;
_locations =
Collections.unmodifiableList(
Arrays.asList(locations));
_description = text;
} // end of FeedScope(int, ClientLocation[], String)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if this scope supports the given
* client location; {@code false} otherwise.
* @param loc check this client location.
* @return {@code true} if client location is supported.
*/
public boolean supports(final ClientLocation loc)
{
return ((_locationMask & loc.mask) != 0);
} // end of supports(ClientLocation)
/**
* Returns the client locations covered by this feed
* scope. The actual locations array is returned and not
* a copy.
* @return covered client locations.
*/
/* package */ List locations()
{
return (_locations);
} // end of locations()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (_description);
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
//-----------------------------------------------------------
// Member data.
//
/**
* Bit mask marking which locations are supported by
* this scope.
*/
private final int _locationMask;
/**
* This feed covers these client locations.
*/
private final List _locations;
/**
* Human-readable text describing this zone.
*/
private final String _description;
} // end of enum FeedScope
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* The default condition always returns true. This default
* is used so that a notification subscription and request
* advertisement always have a non-{@code null} condition,
* removing the need for a {@code if} condition in message
* routing code, which improves JIT performance.
*/
public static final ECondition NO_CONDITION = msg -> true;
/**
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed)}
* method name.
*/
protected static final String NOTIFY_METHOD = "notify";
/**
* Regular expression pattern matching all message key
* subjects. A subject must contain at least one character.
*/
private static final Pattern ALL_SUBJECTS =
Pattern.compile(".+");
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
Logger.getLogger(EFeed.class.getName());
//-----------------------------------------------------------
// Locals.
//
/**
* The client owning this feed. {@link EClient} maintains a
* weak reference to the application instance.
*/
protected final EClient mEClient;
/**
* The feed scope is either limited to this local JVM only,
* remote JVMs only, and both local and remote JVMs.
*/
protected final FeedScope mScope;
/**
* Returns {@code true} if this feed is active, meaning that
* it can still be used by the client. Returns {@code false}
* if this feed is inactive and cannot be used by the client
* again. Once a feed is made inactive, it cannot become
* active again.
*/
protected final AtomicBoolean mIsActive;
/**
* Set to {@code true} when this feed is connected to its
* subject. Initialized to {@code false}.
*/
protected boolean mInPlace;
/**
* Immutable identifier unique within the client. In other
* words, two feeds for two different {@code EClient}
* instances may have the same {@code mFeedId}.
*/
protected final int mFeedId;
/**
* Tracks whether this feed is {@link EFeedState#UP active}
* or {@link EFeedState#DOWN inactive}. Feed state is
* independent of being in place. Whether a feed is in place
* or not, this feed may or may not be active.
*/
protected EFeedState mFeedState;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates an eBus feed for the given client subject, scope,
* and feed type. All callback tasks are posted to
* {@code client}'s
* {@link EClient#dispatch(java.lang.Runnable) run queue}.
* @param client post eBus tasks to this client.
* @param feedScope this feed supports either local, local
* & remote, or just remote feeds.
*/
protected EFeed(final EClient client,
final FeedScope feedScope)
{
mEClient = client;
mScope = feedScope;
mFeedId = client.nextFeedId();
mIsActive = new AtomicBoolean(true);
mInPlace = false;
// Feed state is unknown because it has no initial state.
mFeedState = EFeedState.UNKNOWN;
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("Client %d, Feed %d: opening.",
client.clientId(),
mFeedId));
}
} // end of EFeed(...)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Closes the feed. Performs all necessary work to clean
* up this feed.
*/
protected abstract void inactivate();
//
// end of Abstract Method Declarations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// IEFeed Interface Implementation.
//
/**
* Returns the unique feed identifier. The uniqueness is
* limited to within the client and for the feed lifespan
* only. When a feed is closed, the feed identifier may be
* reused.
* @return the feed identifier.
*/
@Override
public final int feedId()
{
return (mFeedId);
} // end of feedId()
/**
* Returns the feed scope: local only, local & remote, or
* remote only.
* @return feed scope.
*/
@Override
public final FeedScope scope()
{
return (mScope);
} // end of scope()
/**
* Returns the eBus client referenced by this feed.
* @return eBus client.
*/
@Override
public final EClient eClient()
{
return (mEClient);
} // end of eClient()
/**
* Returns {@code true} if this feed is still active and
* {@code false} if inactive. Clients may only use active
* feeds. Once a feed is closed, it is marked inactive and
* may not be used by the client again.
*
* Once a feed is closed, the unique feed identifier may be
* reused by a newly opened feed.
*
* @return {@code true} if this feed is active.
* @see #close()
*/
@Override
public final boolean isActive()
{
return (mIsActive.get());
} // end of isActive()
/**
* Returns {@code true} if this feed is "in place"
* (subscribed or advertised) and {@code false} otherwise.
* @return {@code true} if the feed is in place.
*/
@Override
public final boolean inPlace()
{
return (mInPlace);
} // end of inPlace()
/**
* Returns {@code true} if the feed state is
* {@link EFeedState#UP}; otherwise, returns {@code false}.
* @return {@code true} if the feed state is up.
*/
@Override
public boolean isFeedUp()
{
return (mFeedState == EFeedState.UP);
} // end of isFeedUp()
/**
* Closes this feed, marking it as inactive. If this feed
* is activated, then the feed is de-activated first. The
* feed unique identifier is returned to the available feed
* identifier pool and may be assigned to a newly opened
* feed.
*
* If this feed is already inactive, then does nothing.
*
*/
@Override
public void close()
{
// Is this feed still active?
if (mIsActive.getAndSet(false))
{
// Yes. Make it inactive.
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("Client %d, Feed %d: closing.",
mEClient.clientId(),
mFeedId));
}
inactivate();
// Remove this feed from its client.
mEClient.returnFeedId(mFeedId);
mEClient.removeFeed(this);
}
return;
} // end of close()
//
// end of IEFeed Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Object Method Overrides.
//
/**
* Returns {@code true} if {@code o} is a
* non-{@code null EFeed} instance with equal client and
* feed identifiers. Otherwise returns {@code false}.
* @param o comparison object.
* @return {@code true} if {@code o} is a
* non-{@code null EFeed} instance with equal client and
* feed identifiers.
*/
@Override
public boolean equals(final Object o)
{
boolean retcode = (this == o);
if (!retcode && o instanceof EFeed)
{
final EFeed feed = (EFeed) o;
retcode = (mEClient.clientId() ==
(feed.mEClient).clientId() &&
mFeedId == feed.mFeedId);
}
return (retcode);
} // end of equals(Object)
/**
* Returns the hash of the client and feed identifiers.
* @return hash based on the client and feed identifiers.
*/
@Override
public int hashCode()
{
return (Objects.hash(mEClient.clientId(), mFeedId));
} // end of hashCode()
/**
* Returns a containing the feed message key and data member
* values.
* @return textual representation of this feed.
*/
@Override
public String toString()
{
return (
String.format(
"[%s active=%b, in place=%b, id=%d, state=%s]",
mScope,
mIsActive.get(),
mInPlace,
mFeedId,
mFeedState));
} // end of toString()
//
// end of Object Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns the feed client identifier. This identifier is
* guaranteed unique for the application instance's lifespan.
* The identifier is re-used once eBus detects that the
* application instance is finalized. This integer identifier
* may be used by two different application instances over
* the application run time but only if the two instances
* do not exist at the same time.
* @return unique client identifier.
*/
public final int clientId()
{
return (mEClient.clientId());
} // end of clientId()
/**
* Returns the feed client JVM location:
* {@link EClient.ClientLocation#LOCAL local} to this JVM or
* in a {@link EClient.ClientLocation#REMOTE remote} JVM.
* @return client location.
*/
public final ClientLocation location()
{
return (mEClient.location());
} // end of location()
/**
* Returns the current feed state. A down state means
* that messages may not be sent on or received from this
* feed. An up state means that messages may possibly be
* sent on or received from this feed.
* @return current feed state.
*/
public final EFeedState feedState()
{
return (mFeedState);
} // end of feedState()
/**
* Returns the eBus default dispatcher's name. Used when
* {@link #register(EObject, String) registering} a client
* to the default dispatcher.
* @return default dispatcher name.
*/
public static String defaultDispatcher()
{
return (EClient.defaultDispatcher());
} // end of defaultDispatcher()
/**
* Returns a non-{@code null}, possibly empty, message key
* list taken from the current message key dictionary
* entries.
* @return list message key dictionary entries.
*/
public static List findKeys()
{
return (ESubject.findKeys());
} // end of findKeys()
/**
* Returns a list containing message keys for the given
* message class found in the message key dictionary. The
* message class should reference either a notification or
* request message class since only those classes are stored
* in the message key dictionary. If this is not the case,
* then an empty list is returned.
* @param mc message class.
* @return a non-{@code null} and possibly empty message key
* list.
*/
public static List
findKeys(final Class extends EMessage> mc)
{
return (findKeys(mc, ALL_SUBJECTS));
} // end of findKeys(Class)
/**
* Returns a list containing message keys for the given
* message class and subject pattern found in the message key
* dictionary. The message class should reference either a
* notification or request message class since only those
* classes are stored in the message key dictionary. If this
* is not the case, then an empty list is returned.
* @param mc message class.
* @param query message subject regular expression query.
* @return a non-{@code null} and possibly empty message key
* list.
*/
@SuppressWarnings ("unchecked")
public static List
findKeys(final Class extends EMessage> mc,
final Pattern query)
{
final List retval;
Objects.requireNonNull(mc, "mc is null");
Objects.requireNonNull(query, "query is null");
if (ESystemMessage.class.isAssignableFrom(mc) ||
EReplyMessage.class.isAssignableFrom(mc))
{
retval = Collections.EMPTY_LIST;
}
else
{
// Create the regular expression pattern to find
// the message keys.
final Pattern keyPattern =
Pattern.compile(mc.getName() +
EMessageKey.KEY_IFS +
query.pattern());
retval = ESubject.findKeys(keyPattern);
}
return (retval);
} // end of findKeys(Class, Pattern)
//
// end of Get Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* Adds {@code key} to the eBus message key dictionary if
* {@code key} is not already defined. {@code key} must
* reference either a notification or request message since
* only those message keys are stored in the message key
* dictionary.
* @param key add this message key to the subject
* @throws IllegalArgumentException
* if {@code key} is {@code null} or does not reference
* either a {@link net.sf.eBus.messages.ENotificationMessage}
* or {@link net.sf.eBus.messages.ERequestMessage}.
*/
public static void addKey(final EMessageKey key)
{
Objects.requireNonNull(key, "key is null");
if (key.isSystem() || key.isReply())
{
throw (
new IllegalArgumentException(
String.format(
"%s is not a notification or request",
(key.messageClass()).getName())));
}
ESubject.addSubject(key);
return;
} // end of addKey(EMessageKey)
/**
* Adds the given message key collection to the message key
* dictionary if all keys are not null and reference
* notification and/or request messages. Put conversely, if
* {@code keys} contains a {@code null} or non-notification/
* request key, then none of the keys is placed into the
* message key dictionary.
*
* The listed keys must reference either a notification or
* request message since only those message keys are stored
* in the message key dictionary. The list may consist of a
* mixture of notification and request message keys.
*
* @param keys put these notification and/or request message
* keys into the message key dictionary.
* @throws IllegalArgumentException
* if {@code keys} contains a {@code null} or does not
* reference either a
* {@link net.sf.eBus.messages.ENotificationMessage}
* or {@link net.sf.eBus.messages.ERequestMessage}.
*/
public static void addAllKeys(final Collection keys)
{
Objects.requireNonNull(keys, "keys is null");
// Firstly, validate the keys *before* attempting to add
// them to the dictionary.
keys.forEach(
key ->
{
Objects.requireNonNull(key, "contains null key");
if (key.isSystem() || key.isReply())
{
throw (
new IllegalArgumentException(
String.format(
"%s is not a notification or request",
(key.messageClass()).getName())));
}
});
ESubject.addAllSubjects(keys);
return;
} // end of addAllKeys(Collection<>)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Registers the application object {@code client} with
* eBus, assigning the client to the dispatcher configured
* for the client's class, and using the defined
* {@link EObject#startup()} and {@link EObject#shutdown()}
* methods. Once registered, {@code client} may be
* started using the {@link #startup(EObject)} or
* {@link #startup(List)} methods.
*
* Note: this method must be called
* before {@code client} opens any feeds. Failure to
* do so results in a thrown {@link IllegalStateException}.
*
* @param client register this application object with
* eBus.
* @throws NullPointerException
* if {@code client} is {@code null}.
* @throws IllegalStateException
* if {@code client} is already registered with eBus. This
* will happen if {@code client} has opened any feeds prior
* to making this call.
*
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(EObject)
* @see #shutdown(EObject)
*/
public static void register(final EObject client)
{
Objects.requireNonNull(client, "client is null");
// Use the dispatcher for this client's class and the
// EObject startup and shutdown methods.
final EClient.DispatcherInfo dispatcher =
EClient.findDispatcher(client);
register(client,
dispatcher.name(),
client::startup,
client::shutdown);
return;
} // end of register(EObject)
/**
* Registers the application object {@code client} with
* eBus, assigning {@code client} to the named dispatcher.
* This method allows individual application objects to be
* assigned to a dispatcher rather than by class. The purpose
* here is to allow objects within a class to be assigned to
* different dispatchers based on application need. That is,
* certain objects may be assigned to a higher priority
* dispatcher and the rest assigned to a lower priority
* dispatcher.
*
* Once registered, {@code client} may be started by calling
* {@link #startup(EObject)} or
* {@link #startup(List)} methods which, in turn,
* calls the defined {@link EObject#startup()} method.
*
*
* Note: this method must be called
* before {@code client} opens any feeds. Failure to
* do so results in a thrown {@link IllegalStateException}.
*
* @param client register this application client with eBus.
* @param dispatcherName the dispatcher name.
* @throws NullPointerException
* if {@code client} is {@code null} or
* {@code dispatcherName} is {@code null}
* @throws IllegalArgumentException
* if {@code dispatcherName} is empty or does not reference a
* configured dispatcher.
* @throws IllegalStateException
* if {@code client} is already registered with eBus. This
* will happen if {@code client} has opened any feeds prior
* to making this call.
*/
public static void register(final EObject client,
final String dispatcherName)
{
register(client,
dispatcherName,
client::startup,
client::shutdown);
return;
} // end of register(EObject, String)
/**
* Registers the application object {@code client} with
* eBus, assigning {@code client} to the named dispatcher.
* This method allows individual application objects to be
* assigned to a dispatcher rather than by class. The purpose
* here is to allow objects within a class to be assigned to
* different dispatchers based on application need. That is,
* certain objects may be assigned to a higher priority
* dispatcher and the rest assigned to a lower priority
* dispatcher.
*
* Once registered, {@code client} may be started by calling
* {@link #startup(EObject)} or
* {@link #startup(List)} methods which, in turn,
* calls {@code startCb}.
*
*
* Note: this method must be called
* before {@code client} opens any feeds. Failure to
* do so results in a thrown {@link IllegalStateException}.
*
* @param client register this application client with eBus.
* @param dispatcherName the dispatcher name.
* @param startCb {@code client} start-up method callback.
* @param shutdownCb {@code client} shutdown method callback.
* @throws NullPointerException
* if any of the arguments is {@code null}.
* @throws IllegalArgumentException
* if {@code dispatcherName} is either an empty string or
* does not reference a known dispatcher.
* @throws IllegalStateException
* if {@code client} is already registered with eBus. This
* will happen if {@code client} has opened any feeds prior
* to making this call.
*/
public static void register(final EObject client,
final String dispatcherName,
final Runnable startCb,
final Runnable shutdownCb)
{
Objects.requireNonNull(client, "client is null");
Objects.requireNonNull(dispatcherName,
"dispatcherName is null");
Objects.requireNonNull(startCb, "startCb is null");
Objects.requireNonNull(shutdownCb, "shutdownCb is null");
if (dispatcherName.isEmpty())
{
throw (
new IllegalArgumentException(
"dispatcherName is empty"));
}
final EClient.DispatcherInfo info =
EClient.findDispatcher(dispatcherName);
if (info == null)
{
throw (
new IllegalArgumentException(
dispatcherName + " is an unknown dispatcher"));
}
EClient.addClient(client,
ClientLocation.LOCAL,
info,
startCb,
shutdownCb);
return;
} // end of register(EObject, String)
/**
* Calls the start-up method
* {@link #register(EObject, String, Runnable, Runnable) registered}
* with eBus if-and-only-if the application object is not
* currently started. If application object is started, then
* the start-up method will not be called again.
*
* Note: it is possible to start an application object
* multiple times if that object is shut down between each
* start.
*
*
* The start-up method is called from within the context of
* the object's run queue. Because the application object is
* started on an eBus thread, the start-up method will not be
* run concurrently with any other eBus callback.
*
* @param client start this eBus client.
* @throws NullPointerException
* if {@code client} is {@code null}.
* @throws IllegalStateException
* if {@code client} is not registered with eBus.
*
* @see #register(EObject)
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(List)
* @see #startupAll()
* @see #shutdown(EObject)
* @see #shutdown(List)
* @see #shutdownAll()
*/
public static void startup(final EObject client)
{
final EClient eClient;
Objects.requireNonNull(client, "client is null");
if ((eClient = EClient.findClient(client)) == null)
{
throw (
new IllegalStateException(
"client not registered with eBus"));
}
final List clients = new ArrayList<>();
clients.add(eClient);
EClient.startup(clients);
return;
} // end of startup(EObject)
/**
* Call the registered start-up method for each of the
* application objects in {@code clients} if-and-only-if
* the application object is not currently started. If any
* of the applications is currently started, then that
* objects start-up method will not be called again.
*
* Note: it is possible to start an application object
* multiple times if that object is shut down between each
* start.
*
*
* The start-up method is called from within the context of
* the object's run queue. Because the application object is
* started on an eBus thread, the start-up method will not be
* run concurrently with any other eBus callback.
*
* @param clients start these eBus clients.
* @throws NullPointerException
* if {@code clients} is {@code null} or contains a
* {@code null} entry.
* @throws IllegalStateException
* if {@code clients} contains an entry that is not
* registered with eBus.
*
* @see #register(EObject)
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(EObject)
* @see #startupAll()
* @see #shutdown(EObject)
* @see #shutdown(List)
* @see #shutdownAll()
*/
public static void startup(final List extends EObject> clients)
{
Objects.requireNonNull(clients, "clients is null");
final List eClients = new ArrayList<>();
EClient eClient;
// Validate clients.
for (EObject client : clients)
{
Objects.requireNonNull(
client, "clients contains a null entry");
eClient = EClient.findClient(client);
if (eClient == null)
{
throw (
new IllegalStateException(
"clients contains an unregistered object"));
}
else
{
eClients.add(eClient);
}
}
// The clients set checks out. Start the application
// objects.
EClient.startup(eClients);
return;
} // end of startup(List<>)
/**
* Calls the start-up method for all currently registered
* application objects
* which are not currently started. This method
* is generally called from an application's
* {@code static main} method after having created and
* registered the application's eBus objects.
*
* Note: it is possible to start an application object
* multiple times if that object is shut down between each
* start.
*
*
* The start-up method is called from within the context of
* the object's run queue. Because the application object is
* started on an eBus thread, the start-up method will not be
* run concurrently with any other eBus callback.
*
*
* @see #register(EObject)
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(EObject)
* @see #startup(List)
* @see #shutdown(EObject)
* @see #shutdown(List)
* @see #shutdownAll()
*/
public static void startupAll()
{
EClient.startup(EClient.getClients());
return;
} // end of startupAll()
/**
* Calls the shutdown method
* {@link #register(EObject, String, Runnable, Runnable) registered}
* with eBus if-and-only-if the application object is
* currently started. If application object is not started,
* then the shutdown method will not be called again.
*
* Note: it is possible to shut down an application object
* multiple times if that object is started up between each
* shutdown.
*
*
* The shutdown method is called from within the context of
* the object's run queue. Because the application object is
* stopped on an eBus thread, the shutdown method will not be
* run concurrently with any other eBus callback.
*
* @param client stop this eBus client.
* @throws NullPointerException
* if {@code client} is {@code null}.
* @throws IllegalStateException
* if {@code client} is not registered with eBus.
*
* @see #register(EObject)
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(EObject)
* @see #startup(List)
* @see #startupAll()
* @see #shutdown(List)
* @see #shutdownAll()
*/
public static void shutdown(final EObject client)
{
Objects.requireNonNull(client, "client is null");
final EClient eClient = EClient.findClient(client);
if (eClient == null)
{
throw (
new IllegalStateException(
"client not registered with eBus"));
}
final List clients = new ArrayList<>();
clients.add(eClient);
EClient.shutdown(clients);
return;
} // end of shutdown(EObject)
/**
* Call the registered shutdown method for each of the
* application objects in {@code clients} if-and-only-if
* the application object is currently started. If any
* of the applications is not currently started, then that
* objects shutdown method will not be called again.
*
* Note: it is possible to shut down an application object
* multiple times if that object is started up between each
* shutdown.
*
*
* The shutdown method is called from within the context of
* the object's run queue. Because the application object is
* stopped on an eBus thread, the shutdown method will not be
* run concurrently with any other eBus callback.
*
* @param clients stop these eBus clients.
* @throws NullPointerException
* if {@code clients} is {@code null} or contains a
* {@code null} entry.
* @throws IllegalStateException
* if {@code clients} contains an entry that is not
* registered with eBus.
*
* @see #register(EObject)
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(EObject)
* @see #startup(List)
* @see #startupAll()
* @see #shutdown(EObject)
* @see #shutdownAll()
*/
public static void shutdown(final List extends EObject> clients)
{
Objects.requireNonNull(clients, "clients is null");
final List eClients = new ArrayList<>();
EClient eClient;
// Validate clients.
for (EObject client : clients)
{
Objects.requireNonNull(
client, "clients contains a null entry");
eClient = EClient.findClient(client);
if (eClient == null)
{
throw (
new IllegalStateException(
"clients contains an unregistered object"));
}
else
{
eClients.add(eClient);
}
}
// The clients set checks out. Stop the application
// objects.
EClient.shutdown(eClients);
return;
} // end of shutdown(List<>)
/**
* Calls the shutdown method for all currently registered
* application objects
* which are currently started. This method
* is generally called by an application just prior to
* shutting down.
*
* Note: it is possible to stop an application object
* multiple times if that object is started between each
* shut down.
*
*
* The shutdown method is called from within the context of
* the object's run queue. Because the application object is
* stopped on an eBus thread, the start-up method will not be
* run concurrently with any other eBus callback.
*
*
* @see #register(EObject)
* @see #register(EObject, String)
* @see #register(EObject, String, Runnable, Runnable)
* @see #startup(EObject)
* @see #startup(List)
* @see #startupAll()
* @see #shutdown(EObject)
* @see #shutdown(List)
*/
public static void shutdownAll()
{
EClient.shutdown(EClient.getClients());
return;
} // end of shutdownAll()
/**
* Writes the entire message key dictionary to the given
* object output stream. Only message keys are written to the
* object output stream. The associated eBus subjects and
* their related feeds are not stored. When the message key
* is re-loaded from the object stream at application start,
* the eBus subjects are recreated but not the feeds. Feeds
* must be re-opened by the application upon start.
*
* Caller is responsible for opening {@code oos} prior to
* calling this method and closing {@code oos} after this
* method returns.
*
* @param oos load message keys to this object output stream.
* @throws IOException
* if an error occurs writing message keys to {@code oos}.
*
* @see #storeKeys(Class, ObjectOutputStream)
* @see #storeKeys(Class, Pattern, ObjectOutputStream)
* @see #loadKeys(ObjectInputStream)
*/
public static void storeKeys(final ObjectOutputStream oos)
throws IOException
{
Objects.requireNonNull(oos, "oos is null");
ESubject.storeKeys(oos);
return;
} // end of storeKeys()
/**
* Write those message keys associated with the given message
* class to the object output stream. Only message keys are
* written to the object output stream. The associated eBus
* subjects and their related feeds are not stored. When the
* message key is re-loaded from the object stream at
* application start, the eBus subjects are recreated but not
* the feeds. Feeds must be re-opened by the application upon
* start.
*
* Caller is responsible for opening {@code oos} prior to
* calling this method and closing {@code oos} after this
* method returns.
*
* @param mc store message keys with this message class.
* @param oos load message keys to this object output stream.
* @throws IOException
* if an error occurs writing message keys to {@code oos}.
*
* @see #storeKeys(ObjectOutputStream)
* @see #storeKeys(Class, Pattern, ObjectOutputStream)
* @see #loadKeys(ObjectInputStream)
*/
public static void storeKeys(final Class extends EMessage> mc,
final ObjectOutputStream oos)
throws IOException
{
storeKeys(mc, ALL_SUBJECTS, oos);
return;
} // end of storeKeys(Class)
/**
* Write those message keys associated with the given message
* class and a subject matching the regular expression to the
* object output stream. Only message keys are written to the
* object output stream. The associated eBus subjects and
* their related feeds are not stored. When the message key
* is re-loaded from the object stream at application start,
* the eBus subjects are recreated but not the feeds. Feeds
* must be re-opened by the application upon start.
*
* Caller is responsible for opening {@code oos} prior to
* calling this method and closing {@code oos} after this
* method returns.
*
* @param mc store message keys with this message class.
* @param query store message keys with a subject matching
* this regular expression.
* @param oos load message keys to this object output stream.
* @throws IOException
* if an I/O error occurs when storing the message keys.
*
* @see #storeKeys(ObjectOutputStream)
* @see #storeKeys(Class, ObjectOutputStream)
* @see #loadKeys(ObjectInputStream)
*/
public static void storeKeys(final Class extends EMessage> mc,
final Pattern query,
final ObjectOutputStream oos)
throws IOException
{
Objects.requireNonNull(mc, "mc is null");
Objects.requireNonNull(query, "query is null");
Objects.requireNonNull(oos, "oos is null");
if (ENotificationMessage.class.isAssignableFrom(mc) ||
ERequestMessage.class.isAssignableFrom(mc))
{
// Create the regular expression pattern to find
// the message keys.
final Pattern keyPattern =
Pattern.compile(mc.getName() +
EMessageKey.KEY_IFS +
query.pattern());
ESubject.storeKeys(keyPattern, oos);
}
return;
} // end of storeKeys(Class, Pattern)
/**
* Reads the {@link EMessageKey message keys} contained in
* the given object input stream and loads them back into the
* eBus message key dictionary. eBus subjects are re-created
* but not their associated feeds. The application is
* responsible for re-opening feeds when the application
* starts.
*
* Message keys defined prior to calling this method are
* not overwritten or replaced by duplicates loaded from
* the object input stream.
*
*
* Caller is responsible for opening {@code ois} prior to
* calling this method and closing {@code ois} after this
* method returns.
*
* @param ois read in message keys from this object input
* stream.
* @throws IOException
* if an I/O error occurs reading in the
*/
public static void loadKeys(final ObjectInputStream ois)
throws IOException
{
Objects.requireNonNull(ois, "ois is null");
ESubject.loadKeys(ois);
return;
} // end of loadKeys(ObjectInputStream)
/**
* Returns {@code true} if the application object stored in
* {@code EClient} defines a method with the given name and
* parameters. Returns {@code false} if the application
* object does not define the method or inherits a default
* implementation of the method.
* @param methodName method name.
* @param params method parameters.
* @return {@code true} if {@code clazz} overrides the
* method.
*/
protected final boolean isOverridden(final String methodName,
final Class>... params)
{
boolean retcode = false;
try
{
final Method method =
(mEClient.targetClass()).getMethod(
methodName, params);
retcode = !method.isDefault();
}
catch (NoSuchMethodException | SecurityException jex)
{
// Ignore and return false.
}
return (retcode);
} // end of isOverridden(Class, String, Class...)
} // end of class EFeed