net.sf.eBus.client.EFeed Maven / Gradle / Ivy
//
// Copyright 2015, 2016, 2018, 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 com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.config.EConfigure;
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.ValidationException;
import net.sf.eBus.util.Validator;
import net.sf.eBus.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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-subject 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-subject query feed, the query would find
* no matching keys.
*
*
* Multi-subject feeds act as proxies between the application
* client and the individual {@code EPublishFeed},
* {@code ESubscribeFeed}, {@code ERequestFeed} and
* {@code EReplyFeed} in the multi-subject feed. The
* multi-subject feed opens, advertises/subscribes, and closes
* all subordinate feeds in unison. The individual feeds all
* reference the same client and client callback methods. If a
* multi-subject feed is for 100 subjects, then the client
* receives callbacks from all 100 subordinate feed and
* not for the single multi-subject feed.
*
*
* Note: a multi-subject feed is not
* {@code EFeed} subclass. However, multi-subject 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
{
//---------------------------------------------------------------
// 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 request})
* 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 data.
//
/**
* Bit mask marking which locations are supported by
* this scope.
*/
private final int mLocationMask;
/**
* This feed covers these client locations.
*/
private final List mLocations;
/**
* Human-readable text describing this zone.
*/
private final String mDescription;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a feed scope with the given description.
* @param locationMask bit mask specifying which
* locations are supported by this scope. This is where
* the feed client resides.
* @param locations the client locations associated with
* this feed scope. This is from where messages may be
* posted.
* @param text text describing this feed scope.
*/
private FeedScope(final int locationMask,
final ClientLocation[] locations,
final String text)
{
mLocationMask = locationMask;
mLocations = ImmutableList.copyOf(locations);
mDescription = 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 ((mLocationMask & loc.mask) != 0);
} // end of supports(ClientLocation)
/**
* Returns {@code true} if this scope intersects with the
* given scope; {@code false} otherwise.
* @param scope check for intersection with this scope.
* @return {@code true} if {@code scope} is supported.
*/
public boolean intersects(final FeedScope scope)
{
return ((mLocationMask & scope.mLocationMask) != 0);
} // end of intersects(FeedScope)
/**
* Returns the client locations covered by this feed
* scope. The returned list is immutable.
* @return covered client locations.
*/
/* package */ List locations()
{
return (mLocations);
} // end of locations()
//
// end of Get Methods.
//-------------------------------------------------------
//-------------------------------------------------------
// Object Method Overrides.
//
@Override
public String toString()
{
return (mDescription);
} // end of toString()
//
// end of Object Method Overrides.
//-------------------------------------------------------
} // 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;
/**
* Message associated with attempting to use an inactive
* message feed.
*/
public static final String FEED_IS_INACTIVE =
"feed is inactive";
/**
* Message associated with attempting to use an unadvertised
* message feed.
*/
public static final String FEED_NOT_ADVERTISED =
"feed not advertised";
/**
* Message associated with using a {@code null EClient} .
*/
public static final String NULL_CLIENT = "client is null";
/**
* {@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 =
LoggerFactory.getLogger(EFeed.class);
//-----------------------------------------------------------
// 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;
sLogger.debug("Client {}, Feed {}: opening.",
client.clientId(),
mFeedId);
} // end of EFeed(...)
/**
* Creates an eBus feed based on the given builder
* configuration.
* @param builder contains feed configuration.
*/
protected EFeed(final Builder, ?, ?> builder)
{
mEClient = builder.mEClient;
mScope = builder.mScope;
mFeedId = mEClient.nextFeedId();
mIsActive = new AtomicBoolean(true);
mInPlace = false;
// Feed state is unknown because it has no initial state.
mFeedState = EFeedState.UNKNOWN;
sLogger.debug("Client {}, Feed {}: opening.",
mEClient.clientId(),
mFeedId);
} // end of EFeed(Builder)
//
// 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()
/**
* 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.
*/
@Override
public final EFeedState feedState()
{
return (mFeedState);
} // end of feedState()
/**
* 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.
sLogger.debug("Client {}, Feed {}: closing.",
mEClient.clientId(),
mFeedId);
inactivate();
// Remove this feed from its client.
mEClient.returnFeedId(mFeedId);
mEClient.removeFeed(this);
}
} // 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 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 = ImmutableList.of();
}
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)
/**
* Adds the given subject listener to the listeners list.
* Does nothing if the listener is already registered.
* @param l add this subject listener.
*/
public static void addListener(final ISubjectListener l)
{
ESubject.addListener(l);
} // end of addListener(ISubjectListener)
/**
* Removes subject listener from the listeners list.
* @param l remove this subject listener.
*/
public static void removeListener(final ISubjectListener l)
{
ESubject.removeListener(l);
} // end of removeListener(ISubjectListener)
//
// 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.className())));
}
ESubject.addSubject(key);
} // 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.className())));
}
});
ESubject.addAllSubjects(keys);
} // end of addAllKeys(Collection<>)
/**
* Sets the message exhaust to the given instance. All
* notification, request, and reply messages passing through
* the local eBus are passed to {@code exhaust} via a
* dispatch thread. Caller is responsible for opening and
* closing the exhaust's underlying persistent store
* appropriately.
*
* Note: only one exhaust may be
* configured at a time. When this method is called, the
* current exhaust is replace with {@code exhaust}. If an
* application requires messages to be persisted to multiple
* destinations, then that must be done via the single
* application exhaust.
*
*
* If {@code exhaust} is {@code null} then the default
* exhaust is used. This default does nothing with the given
* message.
*
* @param exhaust message exhaust message. Passing in
* {@code null} results in the default exhaust being used.
*/
public static void setExhaust(final IMessageExhaust exhaust)
{
ESubject.setExhaust(exhaust);
} // end of setExhaust(IMessageExhaust)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Creates a new client dispatcher run queue based on the
* given configuration. This method allows new dispatcher
* run queues to be created dynamically post-JVM start.
* Normally dispatcher configurations are contained in an
* eBus configuration file and loaded at JVM start using the
* Java command line option
* -Dnet.sf.eBus.config.jsonFile=<config file path>
.
* This technique guarantees that eBus Dispatchers are in
* place prior to assigned eBus clients to a Dispatcher.
* But if this is not possible, then {@code createDispatcher}
* may be used to create a Dispatcher dynamically. But care
* must be taken to make sure this is done prior to
* {@link #register(EObject, String) registering} an eBus
* client to that Dispatcher.
*
* See {@link net.sf.eBus.config.EConfigure.Dispatcher} and
* {@link net.sf.eBus.config.EConfigure.DispatcherBuilder}
* for detailed information on how dispatchers work and how
* to build a dispatcher configuration.
*
* @param dispatcher dispatcher configuration.
* @throws NullPointerException
* if {@code dispatcher} is {@code null}.
* @throws IllegalStateException
* if a dispatcher named
* {@link net.sf.eBus.config.EConfigure.Dispatcher#name()}
* already exists.
*
* @see net.sf.eBus.config.EConfigure.Dispatcher
* @see net.sf.eBus.config.EConfigure.DispatcherBuilder
* @see net.sf.eBus.config.EConfigure#dispatcherBuilder()
*/
public static void createDispatcher(final EConfigure.Dispatcher dispatcher)
{
Objects.requireNonNull(dispatcher, "dispatcher is null");
EClient.createDispatcher(dispatcher);
} // end of createDispatcher(Dispatcher)
/**
* 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, NULL_CLIENT);
// 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);
} // 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);
} // 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, NULL_CLIENT);
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);
} // 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, NULL_CLIENT);
if ((eClient = EClient.findClient(client)) == null)
{
throw (
new IllegalStateException(
"client not registered with eBus"));
}
final ImmutableList.Builder clients =
ImmutableList.builder();
sLogger.debug(
"EFeed: starting up client {}.", client.name());
clients.add(eClient);
EClient.startup(clients.build());
} // 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 ImmutableList.Builder nameBuilder =
ImmutableList.builder();
final ImmutableList.Builder eClients =
ImmutableList.builder();
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
{
nameBuilder.add(" " + client.name());
eClients.add(eClient);
}
}
sLogger.debug(
"EFeed: starting up {}.", nameBuilder.build());
// The clients set checks out. Start the application
// objects.
EClient.startup(eClients.build());
} // 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()
{
sLogger.debug(
"EFeed: starting up all registered clients.");
EClient.startup(EClient.getClients());
} // 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, NULL_CLIENT);
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);
} // 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);
} // 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());
} // 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);
} // 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);
} // 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);
}
} // 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);
} // 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...)
/**
* Checks if the message scope and feed scope are in
* agreement. That is, if the message scope is local-only
* but the feed scope is not, then throws an
* {@code IllegalArgumentException}.
* @param key message key.
* @param scope feed scope.
* @throws IllegalArgumentException
* if {@code key} scope is local-only and {@code scope} is
* not {@code FeedScope.LOCAL_ONLY}.
*/
protected static void checkScopes(final EMessageKey key,
final FeedScope scope)
{
if (key.isLocalOnly() && scope != FeedScope.LOCAL_ONLY)
{
throw (
new IllegalArgumentException(
String.format(
"%s is local-only but feed scope is %s",
key,
scope)));
}
} // end of checkScopes(EMessageKey, FeedScope)
/**
* Returns the distance between the given subclass and base
* class. If a direct subclass of base class, then the
* returned value is one. If not a subclass of base class,
* then the returned value is < zero.
* @param subclass check if this class is descended from
* the base class.
* @param bc base class.
* @return distance from subclass to base class or < zero
* if {@code subclass} is not descended from {@code bc}.
*/
protected static int subclassDistance(final Class> subclass,
final Class> bc)
{
Class> sc = subclass;
int distance = 0;
int retval = -1;
while ((sc = sc.getSuperclass()) != null && retval < 0)
{
++distance;
if (bc.equals(sc))
{
retval = distance;
}
}
return (retval);
} // end of subclassDistance(Class, Class)
//---------------------------------------------------------------
// Inner classes.
//
/**
* Base class for eBus client callback tasks created by
* feeds. Contains the eBus feed object.
*/
protected abstract static 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();
sLogger.trace(this.toString());
if (target != null)
{
try
{
mCallback.call(mFeedState, (T) mFeed);
}
catch (Throwable tex)
{
final String reason =
"{} publish status callback exception";
final String className =
(target.getClass()).getName();
if (sLogger.isDebugEnabled())
{
sLogger.warn(reason, className, tex);
}
else
{
sLogger.warn(reason, className);
}
}
}
} // end of run()
//
// end of Runnable Interface Implementation.
//-------------------------------------------------------
} // end of StatusTask
/**
* This task forwards a notification message to
* {@link ESubscriber#notify(ENotificationMessage, IESubscribeFeed)}.
*/
protected 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(...)
/**
* Creates a notify callback task for the given
* condition, feed, and message callback.
* @param condition subscription condition.
* @param feed message is from this feed.
* @param cb notification message callback.
*/
protected NotifyTask(final ECondition condition,
final IESubscribeFeed feed,
final NotifyCallback cb)
{
super (feed);
mMessage = null;
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
@SuppressWarnings ("unchecked")
public void run()
{
final Object target = mEClient.target();
final ENotificationMessage message = message();
if (target != null && message != null)
{
sLogger.trace(
"NotifyTask[key={}]", message.key());
try
{
// Is the subscriber interested in this
// message?
if (mCondition.test(message))
{
// Yes. Forward the notification message
// to the subscriber.
mCallback.call(
message, (IESubscribeFeed) mFeed);
}
}
catch (Throwable tex)
{
final String reason =
"NotifyTask[{}, {}] exception";
final String className =
(target.getClass()).getName();
final EMessageKey key = message.key();
if (sLogger.isDebugEnabled())
{
sLogger.warn(
reason, className, key, tex);
}
else
{
sLogger.warn(reason, className, key);
}
}
}
} // end of run()
//
// end of Runnable Interface Implementations.
//-------------------------------------------------------
/**
* Returns message to be forwarded to target.
* @return forwarded message.
*/
protected ENotificationMessage message()
{
return (mMessage);
} // end of message()
} // end of class NotifyTask
/**
* Base class for all {@link EFeed} builders. Contains the
* configuration fields common to all feed types.
*
* @param feed type.
* @param {@link EObject} sub-type.
* @param builder leaf type.
*/
protected abstract static class Builder>
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Instance feed class returned by {@link #build}. This
* value is used to identify the target class when a
* build failure occurs, placed in the
* {@link ValidationException}.
*/
protected final Class mTargetClass;
/**
* Matching feed messages are forwarded to this eBus
* client. Required for all feeds.
*/
protected EObject mTarget;
/**
* Target instance location. Defaults to
* {@link ClientLocation#LOCAL}.
*/
protected ClientLocation mLocation;
/**
* Publish or subscribe to messages within this feed
* scope. Required for all feeds.
*/
protected FeedScope mScope;
/**
* eBus client containing the target object.
*/
protected EClient mEClient;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new feed builder for the given target class.
* @param targetClass target class receiving feed
* messages.
*/
protected Builder(final Class targetClass)
{
mTargetClass = targetClass;
mLocation = ClientLocation.LOCAL;
} // end of Builder(Class)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Returns an eBus feed instance build from the
* configured properties. Note: the
* builder configuration was
* {@link #validate(Validator) validated}
* prior to calling this method. The message object may
* now be constructed.
* @return target feed instance.
*/
protected abstract F buildImpl();
/**
* Returns {@code Builder this} pointer as the leaf
* builder class instance.
* @return leaf {@code Builder} instance.
*/
protected abstract B self();
//
// end of Abstract Method Declarations.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets eBus object which is the target for inbound
* messages. Returns {@code this Builder} so that
* configuration method calls may be chained.
* @param target eBus target object.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code target} is {@code null}.
*/
public B target(final T target)
{
mTarget =
Objects.requireNonNull(target, "target is null");
return (self());
} // end of target(T)
/**
* Sets eBus target object location to given value. This
* is a package-level method since only
* {@link ERemoteApp} is allowed to access it.
* @param location eBus target object location.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code location} is {@code null}.
*/
/* package */ final B location(final ClientLocation location)
{
mLocation =
Objects.requireNonNull(
location, "location is null");
return (self());
} // end of location(ClientLocation)
/**
* Sets feed scope to either local only, remote only, or
* local and remote.
* @param scope feed receives only those messages within
* this scope.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code scope} is {@code null}.
*/
public B scope(final FeedScope scope)
{
mScope =
Objects.requireNonNull(scope, "scope is null");
return (self());
} // end of scope(FeedScope)
//
// end of Set Methods.
//-------------------------------------------------------
/**
* Returns target feed instance constructed from the
* configured properties. This must be the final
* method in a builder configuration call chain.
* @return target eBus feed instance.
* @throws ValidationException
* if {@code this} builder does not contain a valid
* target feed configuration. This exception contains a
* list of all configuration problems found.
*/
public final F build()
throws ValidationException
{
final Validator problems = new Validator();
final F retval;
// Check if this builder contains valid settings.
validate(problems);
// Were any problems found?
if (!problems.isEmpty())
{
// Yes.
throw (
new ValidationException(
mTargetClass, problems.errors()));
}
// Find or create the eBus client for the configured
// target *before* building the feed because builder
// sub-classes depend on this.
mEClient =
EClient.findOrCreateClient(mTarget, mLocation);
retval = buildImpl();
mEClient.addFeed(retval);
// No problems found. Return the new feed instance.
return (retval);
} // end of build()
/**
* Checks if target eBus object and feed scope are
* configured. Also checks if message key is
*
* This method should be overridden by subclass feed
* builders and called before doing its own
* validation. The first line in the subclass
* {@code validate} implementation should be
* {@code super.validate(problems);}.
*
* @param problems place invalid configuration settings
* in this problems list.
* @return {@code problems} to allow for method chaining.
*/
protected Validator validate(final Validator problems)
{
return (problems.requireNotNull(mTarget, "target")
.requireNotNull(mScope, "scope"));
} // end of validate(Validator)
/**
* Returns {@code true} if the target eBus object defines
* a method with the given name and parameters. Returns
* {@code false} if the target 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 =
(mTarget.getClass()).getMethod(
methodName, params);
retcode = !method.isDefault();
}
catch (NoSuchMethodException | SecurityException jex)
{
// Ignore and return false.
}
return (retcode);
} // end of isOverridden(String, Class...)
} // end of class BuilderPPPP
} // end of class EFeed