
net.sf.eBus.client.EMultiFeed 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 2017. Charles W. Rapp
// All Rights Reserved.
//
package net.sf.eBus.client;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.util.TernarySearchTree;
import net.sf.eBus.util.regex.Pattern;
/**
* Multiple key feeds act as a proxy between application objects
* and multiple subordinate simple feeds ({@link EPublishFeed},
* {@link ESubscribeFeed}, {@link EReplyFeed}, and
* {@link ERequestFeed}), all for a single message class. The
* multi-key feed keeps the subordinate feeds in the same state:
* open, advertised/subscribed, or closed. The subordinate feeds
* are configured with the same eBus client and callback methods.
*
* This base class tracks all currently open subordinate feeds
* and the overall feed state.
*
*
* @param all feeds are for this message type.
* @param the subordinate feed type.
*
* @author Charles W. Rapp
*/
public abstract class EMultiFeed
extends EFeed
{
//---------------------------------------------------------------
// Inner classes.
//
/**
* This interface is used by {@code EMultiFeed} to
* instantiate a new subordinate feed for a multi-key feed.
* @param client role for the subordinate feed.
* @param subordinate feed type.
*/
@FunctionalInterface
protected static interface
SubordinateFeedFactory
{
/**
* Returns a newly instantiated {@link EFeed}-subclass
* instance based on the given parameters.
* @param client application object opening the multi-key
* feed.
* @param key open subordinate feed for this message
* key.
* @param scope feed scope is local, remote, or both.
* @param condition optional feed condition. Ignored if
* subordinate feed does not support conditions.
* @param location client location. Will always be
* {@link ClientLocation#LOCAL local} because only local
* clients may create multi-key feeds.
* @return new subordinate feed.
*/
F newFeed(R client,
EMessageKey key,
FeedScope scope,
ECondition condition,
ClientLocation location);
} // end of SubordinateFeedFactory
/**
* This interface is used by {@code EMultiFeed} to
* instantiate a new multi-key, sub-class feed.
* @param multi-key feed class.
* @param feed message class.
* @param subordinate feed class.
*/
@FunctionalInterface
protected static interface
MultiFeedFactory
{
/**
* Returns a newly instantiated multi-key feed based on
* the given parameters.
* @param client eBus client encapsulating the
* application object.
* @param mc feed message class.
* @param scope feed scope is local, remote, or both.
* @param condition optional feed condition. Ignored if
* subordinate feed does not support conditions.
* @param feeds initial subordinate feeds.
* @return new multi-key feed.
*/
M newFeed(EClient client,
Class extends C> mc,
FeedScope scope,
ECondition condition,
Map feeds);
} // end of MultiFeedFactory
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link #key()} returns a message key with the subject
* {@value}.
*/
public static final String MULTIFEED_SUBJECT = "__MULTIFEED__";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface. Shared by all subclasses.
*/
protected static final Logger sLogger =
Logger.getLogger(EFeed.class.getName());
//-----------------------------------------------------------
// Locals.
//
/**
* All subordindate feeds apply to this message class. This
* message class is used when {@link #addFeed(String) adding}
* new feeds to this multi-key feed.
*/
protected final Class extends C> mMsgClass;
/**
* Use this condition to check if received message should be
* forwarded to client. This condition is applied to all
* subordinate feeds which use conditions. This data member
* is {@code null} if the subordinate feed does not support
* message condition.
*/
protected final ECondition mCondition;
/**
* Contains the subordinate feeds currently opened by this
* multi-key feed. Maps the {@link EMessageKey#subject} to
* the subordinate feed.
*/
protected final Map mFeeds;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new multiple key feed for the given eBus client,
* message class, scope, and initial feeds. Subordinate feeds
* use the same {@code client}, {@code msgClass},
* {@code scope}, and (optionally) {@code condition}.
* @param client connect this client to the subordinate feeds.
* @param msgClass all feeds apply to this message class.
* @param scope feed scope.
* @param feeds initial subordinate feeds.
* @param condition condition applied to all subordinate
* feeds. Will be {@code null} for feeds which do not use a
* condition.
*/
protected EMultiFeed(final EClient client,
final Class extends C> msgClass,
final FeedScope scope,
final ECondition condition,
final Map feeds)
{
super (client, scope);
mMsgClass = msgClass;
mFeeds = feeds;
mCondition = condition;
} // end of EMultiFeed()
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Returns a newly minted subordinate feed for the given
* message key. This method is used by
* {@link #addFeed(String)} to create a subordinate feed.
* @param key create feed for this key.
* @return a subordinate feed.
*/
protected abstract F createFeed(final EMessageKey key);
/**
* Advertises/subscribes the given feed. This method is
* used by {@link #addFeed(String)} to adverise/subscribe
* the added feed.
* @param feed feed to advertised/subscribed.
*/
protected abstract void putFeedInPlace(final F feed);
//
// end of Abstract Method Declarations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Closes all currently open subordinate feeds and clears
* the feeds map.
*/
@Override
protected final void inactivate()
{
mFeeds.values().forEach(EFeed::close);
mFeeds.clear();
return;
} // end of inactivate()
//
// end of Abstract Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns the multi-key feed message class. All
* {@link #addFeed(String) added} feeds are created with this
* class.
* @return feed message class.
*/
public final Class extends C> messageClass()
{
return (mMsgClass);
} // end of messageClass()
/**
* Returns {@code true} if all subordinate feeds are up and
* {@code false} otherwise.
* @return {@code true} if all subordinate feeds are up.
*/
@Override
public final boolean isFeedUp()
{
final Iterator fit = (mFeeds.values()).iterator();
boolean retcode = true;
while (fit.hasNext() && retcode)
{
retcode = (fit.next()).isFeedUp();
}
return (retcode);
} // end of isFeedUp()
/**
* Returns a message key with {@link #messageClass()} as the
* class and {@link #MULTIFEED_SUBJECT} as the subject.
* @return multi-feed message key.
*/
@Override
public final EMessageKey key()
{
return (new EMessageKey(mMsgClass, MULTIFEED_SUBJECT));
} // end of key()
/**
* Returns the specified feed's 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.
*
* If {@code subject} references an unknown feed or the feed
* is not in place, then {@link EFeedState#UNKNOWN} is
* returned.
*
* @param subject return the publish state for this message
* subject.
* @return the specified feed's state or {@code UNKNOWN} if
* {@code subject} does not reference a known feed.
* @throws NullPointerException
* if {@code subject} is {@code null}.
* @throws IllegalArgumentException
* if {@code subject} is empty.
* @throws IllegalStateException
* if this multi-key feed is closed.
*/
public final EFeedState feedState(final String subject)
{
Objects.requireNonNull(subject, "subject is null");
if (subject.isEmpty())
{
throw (
new IllegalArgumentException(
"subject is an empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
final F feed = mFeeds.get(subject);
return (feed == null ?
EFeedState.UNKNOWN :
feed.feedState());
} // end of feedState(String)
/**
* Returns the current subordinate feed message subjects.
* The returned list is a copy of the actual message subjects
* list. Modifying the returned list has no impact on eBus
* operations.
* @return subordinate feed message subjects.
* @throws IllegalStateException
* if this multi-key feed is closed.
*/
public final List subjects()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
final List retval =
new ArrayList<>(mFeeds.size());
mFeeds.values()
.forEach(
feed -> retval.add((feed.key()).subject()));
return (retval);
} // end of subjects()
/**
* Returns the current subordinate feed message keys. The
* returned list is a copy of the actual message key list.
* Modifying the returned list has no impact on eBus
* operations.
* @return subordinate feed message keys.
* @throws IllegalStateException
* if this multi-key feed is closed.
*/
public final List keys()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
final List retval =
new ArrayList<>(mFeeds.size());
mFeeds.values().forEach(feed -> retval.add(feed.key()));
return (retval);
} // end of keys()
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Adds a new feed based on the configured message class and
* the given subject. If this multi-key feed is advertised or
* subscribed, then the new subordinate feed is
* advertised/subscribed. This method does nothing if there
* is a subordinate feed for {@code subject} in place.
*
* The newly created feed references the same
* {@link #mEClient client}, {@link #mMsgClass message class},
* {@link #mScope scope} and
* (optional) {@link ECondition condition} as all the other
* subordinate feeds in this multi-key feed. It will also use
* the same callbacks as existing subordinate feeds.
*
* @param subject add key with this message subject.
* @throws NullPointerException
* if {@code subject} is {@code null}.
* @throws IllegalArgumentException
* if {@code subject} is empty.
* @throws IllegalStateException
* if this multi-key feed is closed.
*
* @see #addAllFeeds(List)
* @see #addAllFeeds(Pattern)
* @see #closeFeed(String)
*/
public final void addFeed(final String subject)
{
Objects.requireNonNull(subject, "subject is null");
if (subject.isEmpty())
{
throw (
new IllegalArgumentException(
"subject is an empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
// Is this key unique?
if (!mFeeds.containsKey(subject))
{
// Yes, this is a new feed.
final F feed =
createFeed(
new EMessageKey(mMsgClass, subject));
mFeeds.put(subject, feed);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s multi-key feed %d (%s): adding feed %s.",
mEClient.location(),
mEClient.clientId(),
mScope,
subject));
}
// Is this multikey feed subscribed?
if (mInPlace)
{
// Yes, put this new feed in place.
putFeedInPlace(feed);
}
}
return;
} // end of addFeed(String)
/**
* Adds new feeds based on the configured message class and
* the subjects list. If this multi-key feed is in place
* (advertised or subscribed), then the new subordinate feeds
* are put in place as well. If {@code subjects} contains
* feeds that already exist, the existing feed is neither
* replaced or modified.
*
* The newly created feeds reference the same
* {@link #mEClient client}, {@link #mMsgClass message class},
* {@link #mScope scope} and
* (optional) {@link ECondition condition} as all the other
* subordinate feeds in this multi-key feed. They will also
* use the same callbacks as existing subordinate feeds.
*
* @param subjects create new feeds for these subjects.
* @throws NullPointerException
* if {@code subjects} is {@code null} or contains a
* {@code null} element.
* @throws IllegalArgumentException
* if {@code subjects} contains an empty string.
* @throws IllegalStateException
* if this multi-key feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(Pattern)
* @see #closeFeed(String)
*/
public final void addAllFeeds(final List subjects)
{
Objects.requireNonNull(subjects, "subjects is null");
// Are all the subject non-null and not empty?
if (subjects.contains(null))
{
throw (
new NullPointerException(
"subjects contains a null value"));
}
if (subjects.contains(""))
{
throw (
new IllegalArgumentException(
"subjects contains an empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
addFeeds(subjects);
return;
} // end of addAllFeeds(List)
/**
* Adds new feeds for each of the subjects matching the given
* query. If there are no matching subjects, then no feeds
* are added. If {@code query} matches an existing feed
* subject, then the existing feed is neither modified not
* replaced. The new feeds are for this multi-key feed's
* message class.
* @param query find all subjects matching this query.
* @throws NullPointerException
* if {@code query} is {@code null}.
* @throws IllegalStateException
* if this multi-key feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(List)
* @see #closeFeed(String)
*/
public final void addAllFeeds(final Pattern query)
{
Objects.requireNonNull(query, "query is null");
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
// Find the message keys matching the given regular
// expression query.
final Pattern keyPattern =
Pattern.compile(mMsgClass.getName() +
EMessageKey.KEY_IFS +
query.pattern());
final List subjects = new ArrayList<>();
// Extract the subjects from the matching message keys.
ESubject.findKeys(keyPattern)
.forEach(key -> subjects.add(key.subject()));
addFeeds(subjects);
return;
} // end of addAllFeeds(Pattern)
/**
* Removes and closes the feed for the given message subject.
* Does nothing if {@code subject} does not reference a
* known subordinate feed.
* @param subject remove and close the feed for this subject.
* @throws NullPointerException
* if {@code subject} is {@code null}.
* @throws IllegalArgumentException
* if {@code subject} is empty.
* @throws IllegalStateException
* if this multi-key feed is closed.
*
* @see #addFeed(String)
*/
public final void closeFeed(final String subject)
{
Objects.requireNonNull(subject, "subject is null");
if (subject.isEmpty())
{
throw (
new IllegalArgumentException(
"subject is an empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-key feeds.
throw (
new IllegalStateException("feed is inactive"));
}
// Is this a known subordinate feed?
if (mFeeds.containsKey(subject))
{
// Yes. Remove and close the subordinate feed.
(mFeeds.remove(subject)).close();
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s multi-key feed %d (%s): removed feed %s.",
mEClient.location(),
mEClient.clientId(),
mScope,
subject));
}
}
return;
} // end of closeFeed(String)
/**
* Returns a new, opened multi-key feed for the given
* parameters. Does the actual work of validating the
* parameters and creating the initial subordinate feeds.
* @param eBus role used with this multi-key feed.
* @param feed based on this message class.
* @param subordinate feed class.
* @param multi-key feed class.
* @param client application object interacting with the
* feed.
* @param mc feed message class.
* @param subjects message subject list. May not contain
* null or empty strings.
* @param scope whether the feed supports local feeds,
* remote feeds, or both.
* @param condition the optional condition used by the
* subordinate feed.
* @param subFactory used to instantiate a new subordinate
* feed.
* @param multiFactory used to instantiate a new multi-key
* feed.
* @return a new, open multiple key feed for the given
* application object, message class, and subjects.
* @throws NullPointerException
* if any of the arguments is {@code null}.
* @throws IllegalArgumentException
* if {@code subjects} contains an empty string.
*/
protected static >
M openList(final R client,
final Class extends C> mc,
final List subjects,
final FeedScope scope,
final ECondition condition,
final SubordinateFeedFactory subFactory,
final MultiFeedFactory multiFactory)
{
// Validate the parameters.
// Are required parameters non-null references?
Objects.requireNonNull(client, "client is null");
Objects.requireNonNull(mc, "mc is null");
Objects.requireNonNull(subjects, "subjects is null");
Objects.requireNonNull(scope, "scope is null");
// Are all the subject non-null and not empty?
if (subjects.contains(null))
{
throw (
new NullPointerException(
"subjects contains a null value"));
}
if (subjects.contains(""))
{
throw (
new IllegalArgumentException(
"subjects contains an empty string"));
}
return (openMultiFeed(client,
mc,
subjects,
scope,
condition,
subFactory,
multiFactory));
} // end of openList(...)
/**
* Returns a new, opened multi-key feed for the given message
* class and subject regular expression query. A subordinate
* feed is opened for all message keys matching the message
* class and subject query. If there are no matching message
* keys, then the feed is opened with no initial subordinate
* feeds.
* @param eBus role used with this multi-key feed.
* @param feed based on this message class.
* @param subordinate feed class.
* @param multi-key feed class.
* @param client application object interacting with the
* feed.
* @param mc feed message class.
* @param query message subject regular expression query.
* @param scope whether the feed supports local feeds,
* remote feeds, or both.
* @param condition the optional condition used by the
* subordinate feed.
* @param subFactory used to instantiate a new subordinate
* feed.
* @param multiFactory used to instantiate a new multi-key
* feed.
* @return a new, open multiple key feed for the given
* application object, message class, and subject query. May
* have no feeds initially.
* @throws NullPointerException
* if any of the arguments is {@code null}.
*/
protected static >
M openQuery(final R client,
final Class extends C> mc,
final Pattern query,
final FeedScope scope,
final ECondition condition,
final SubordinateFeedFactory subFactory,
final MultiFeedFactory multiFactory)
{
// Validate the parameters.
// Are required parameters non-null references?
Objects.requireNonNull(client, "client is null");
Objects.requireNonNull(mc, "mc is null");
Objects.requireNonNull(query, "query is null");
Objects.requireNonNull(scope, "scope is null");
// Find the message keys matching the given regular
// expression query.
final Pattern keyPattern =
Pattern.compile(mc.getName() +
EMessageKey.KEY_IFS +
query.pattern());
final List subjects = new ArrayList<>();
// Extract the subjects from the matching message keys.
ESubject.findKeys(keyPattern)
.forEach(key -> subjects.add(key.subject()));
return (openMultiFeed(client,
mc,
subjects,
scope,
condition,
subFactory,
multiFactory));
} // end of openQuery(...)
/**
* Performs the actual work of opening a new multi-key feed
* using the given parameters. Creates the initial
* subordinate feeds for each {@code subject}.
* @param
* @param
* @param
* @param
* @param client
* @param mc
* @param subjects
* @param scope
* @param condition
* @param subFactory
* @param multiFactory
* @return
*/
private static >
M openMultiFeed(final R client,
final Class extends C> mc,
final List subjects,
final FeedScope scope,
final ECondition condition,
final SubordinateFeedFactory subFactory,
final MultiFeedFactory multiFactory)
{
final ClientLocation loc = ClientLocation.LOCAL;
final EClient eClient =
EClient.findOrCreateClient(client, loc);
final Map feeds =
new TernarySearchTree<>();
final M retval;
// Create the initial subordinate feeds.
subjects.forEach(
subject ->
feeds.put(
subject,
subFactory.newFeed(
client,
new EMessageKey(mc, subject),
scope,
condition,
loc)));
retval = multiFactory.newFeed(eClient,
mc,
scope,
condition,
feeds);
eClient.addFeed(retval);
return (retval);
} // end of openMultiFeed(...)
/**
* Creates the new feeds for each of the list subjects.
* @param subjects new feed subjects.
*/
private void addFeeds(final List subjects)
{
subjects.stream()
// Filter out duplicate feed subjects.
.filter(s -> (!mFeeds.containsKey(s)))
.forEachOrdered(
s ->
{
final F feed =
createFeed(
new EMessageKey(mMsgClass, s));
mFeeds.put(s, feed);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"%s multi-key feed %d (%s): adding feed %s.",
mEClient.location(),
mEClient.clientId(),
mScope,
s));
}
// Is this multikey feed subscribed?
if (mInPlace)
{
// Yes, put this new feed in place.
putFeedInPlace(feed);
}
});
return;
} // end of addFeeds(List<>)
} // end of class EMultiFeed