
net.sf.eBus.client.EMultiFeed Maven / Gradle / Ivy
//
// Copyright 2017, 2023 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.base.Strings;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nullable;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.messages.ELocalOnly;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.util.TernarySearchTree;
import net.sf.eBus.util.Validator;
import net.sf.eBus.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Multiple subject 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-subject feed keeps subordinate
* feeds in the same state: open, advertised/subscribed, or
* closed. The subordinate feeds are configured with the same
* eBus client and callback methods.
*
* A multi-subject feed can be created by defining either a
* {@code List} subjects list or providing a
* {@code Pattern} subject query. A subject list defines a
* static, fixed set of subjects. This subject list will not
* change except by explicitly adding new subjects (see
* {@link #addFeed(String)} and {@link #addFeeds(List)}).
*
*
* A subject query defines a dynamic subject set.
* Initially this query is matched against existing subjects
* for the given message class. When new subjects are detected
* and
*
* -
* the multi-subject feed is open,
*
* -
* the new feed's message class is the same as the
* multi-subject feed, and
*
* -
* the subject matches the subject query,
*
*
* then the new subject is added to the multi-subject feed and
* moved to the same state as existing subjects.
*
*
* New subjects may be added to an open multi-subject feed using
* the {@link #addFeed(String)}, {@link #addAllFeeds(List)}, and
* {@link #addAllFeeds(Pattern)}. These new subordinate feeds are
* moved to the same state as existing subjects.Conversely,
* subordinate feeds are removed and closed using the
* {@link #closeFeed(String)}, {@link #closeAllFeeds(List)}, and
* {@link #closeAllFeeds(Pattern)}.
*
*
* Note when using a query to add new subjects, this query is
* used only for that {@code addAllFeeds} method call and
* not used against new subjects created after the
* method returns.
*
*
* See the subordinate publisher, subscriber, requester, and
* replier multi-subject feeds for a detailed example using
* each feed type.
*
*
* @param all feeds are for this message type.
* @param the subordinate feed type.
*
* @see EMultiPublishFeed
* @see EMultiSubscribeFeed
* @see EMultiRequestFeed
* @see EMultiReplyFeed
*
* @author Charles W. Rapp
*/
public abstract class EMultiFeed
extends EFeed
implements ISubjectListener
{
//---------------------------------------------------------------
// Enums.
//
/**
* A multi-feed either supports subject list or a subject
* pattern.
*/
public enum MultiFeedType
{
/**
* Multi-feed type not yet set.
*/
NOT_SET,
/**
* Create a multi-feed for a given subject list.
*/
LIST,
/**
* Create a multi-feed for a given subject pattern.
*/
QUERY
} // end of enum MultiFeedType
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* {@link #key()} returns a message key with the subject
* {@value}.
*/
public static final String MULTIFEED_SUBJECT = "__MULTIFEED__";
private static final String NULL_SUBJECTS =
"subjects is null";
private static final String SUBJECTS_CONTAINS_NULL =
"subjects contains a null value";
private static final String SUBJECTS_CONTAINS_EMPTY_STRING =
"subjects contains an empty string";
private static final String NULL_QUERY = "query is null";
//-----------------------------------------------------------
// Statics.
//
/**
* Logging subsystem interface. Shared by all subclasses.
*/
protected static final Logger sLogger =
LoggerFactory.getLogger(EMultiFeed.class);
//-----------------------------------------------------------
// Locals.
//
/**
* All subordinate feeds apply to this message class. This
* message class is used when {@link #addFeed(String) adding}
* new feeds to this multi-subject feed.
*/
protected final Class extends C> mMsgClass;
/**
* Query used to select subjects based on a a given pattern.
* Will be {@code null} if this multi-subject feed is list
* based.
*/
protected final @Nullable Pattern mQuery;
/**
* Contains the subordinate feeds currently opened by this
* multi-subject feed. Maps the {@link EMessageKey#subject}
* to the subordinate feed.
*/
protected final Map mFeeds;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new multi-publish feed instance based on the
* validated builder settings.
* @param builder contains validated feed configuration.
*/
@SuppressWarnings ("unchecked")
protected EMultiFeed(final Builder builder)
{
super (builder);
mMsgClass = builder.mMsgClass;
mQuery = builder.mQuery;
mFeeds = builder.mFeeds;
} // end of EMultiFeed(Builder)
//
// 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();
} // end of inactivate()
//
// end of Abstract Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// ISubjectListener Interface Implementation.
//
/**
* If the new subject's message type and subject matches
* this feed's message type and subject query, then add
* create a new subordinate feed and put that new feed
* into the proper state.
* @param subjectType subject is either for a notification
* or request message.
* @param subjectKey newly added subject's message key.
*/
@Override
public final void subjectUpdate(final SubjectType subjectType,
final EMessageKey subjectKey)
{
final String subject = subjectKey.subject();
// Is this feed still active?
// Is this the correct message class?
// Does the subject match the multi-subject query?
// Is this a unique subject?
if (mIsActive.get() &&
mMsgClass.equals(subjectKey.messageClass()) &&
mQuery.matches(subject) &&
!mFeeds.containsKey(subject))
{
// Yes. Create the subordinate feed.
doAddFeed(subject);
}
} // end of subjectUpdate(SubjectType, EMessageKey)
//
// end of ISubjectListener Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns the multi-subject 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-subject feed is closed.
*/
public final EFeedState feedState(final String subject)
{
if (Strings.isNullOrEmpty(subject))
{
throw (
new IllegalArgumentException(
"subject is a null or empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// 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 {@code true} if the given subject is part of this
* multi-feed.
* @param subject check if this subject is in the feed.
* @return {@code true} if subject is part of multi-feed.
*/
public final boolean isSubject(final String subject)
{
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
return (mFeeds.containsKey(subject));
} // end of isSubject(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-subject feed is closed.
*/
public final List subjects()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// 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-subject feed is closed.
*/
public final List keys()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// 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()
/**
* Returns a mapping between the current subordinate feed
* message keys and the subordinate feed state. The returned
* map may be freely modified without impact to this
* multi-feed data members.
* @return state of current subordinate feeds.
*/
public final Map feedStates()
{
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
final Map retval =
new HashMap<>(mFeeds.size());
mFeeds.values()
.forEach(
feed ->
retval.put(feed.key(), feed.feedState()));
return (retval);
} // end of feedStates()
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Adds a new feed based on the configured message class and
* the given subject. If this multi-subject 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-subject feed. It will also
* use the same callbacks as existing subordinate feeds.
*
*
* Individual subordinate feeds are removed and closed using
* the {@link #closeFeed(String)} method.
*
* @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-subject feed is closed.
*
* @see #addAllFeeds(List)
* @see #addAllFeeds(Pattern)
* @see #closeFeed(String)
* @see #closeAllFeeds(List)
* @see #closeAllFeeds(Pattern)
*/
public final void addFeed(final String subject)
{
if (Strings.isNullOrEmpty(subject))
{
throw (
new IllegalArgumentException(
"subject is either null or an empty string"));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
// Is this key unique?
if (!mFeeds.containsKey(subject))
{
// Yes, this is a new feed.
doAddFeed(subject);
}
} // end of addFeed(String)
/**
* Adds new feeds based on the configured message class and
* the subjects list. If this multi-subject 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-subject 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-subject feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(Pattern)
* @see #closeFeed(String)
* @see #closeAllFeeds(List)
*/
public final void addAllFeeds(final List subjects)
{
Objects.requireNonNull(subjects, NULL_SUBJECTS);
// Are all the subject non-null and not empty?
if (subjects.contains(null))
{
throw (
new NullPointerException(
SUBJECTS_CONTAINS_NULL));
}
if (subjects.contains(""))
{
throw (
new IllegalArgumentException(
SUBJECTS_CONTAINS_EMPTY_STRING));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
addFeeds(subjects);
} // 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-subject feed's
* message class.
*
* Note: this query is not
* persisted nor used to match against new feed subjects
* created after this method returns. This is unlike
* a query defined when creating a multi-subject feed. This
* initial query is persisted and matched against new
* subjects.
*
* @param query find all subjects matching this query.
* @throws NullPointerException
* if {@code query} is {@code null}.
* @throws IllegalStateException
* if this multi-subject feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(List)
* @see #closeFeed(String)
* @see #closeAllFeeds(List)
* @see #closeAllFeeds(Pattern)
*/
public final void addAllFeeds(final Pattern query)
{
Objects.requireNonNull(query, NULL_QUERY);
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// 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);
} // 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-subject feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(List)
* @see #addAllFeeds(Pattern)
* @see #closeAllFeeds(List)
* @see #closeAllFeeds(Pattern)
*/
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-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
// Is this a known subordinate feed?
if (mFeeds.containsKey(subject))
{
// Yes. Remove and close the subordinate feed.
doCloseFeed(subject);
}
} // end of closeFeed(String)
/**
* Removes and closes all individual feeds for the given
* subject list. Does nothing if a listed subject does not
* reference a known subordinate feed.
* @param subjects remove and close all lists subject feeds.
* @throws NullPointerException
* if {@code subjects} is {@code null} or contains a
* {@code null} subject.
* @throws IllegalArgumentException
* if {@code subjects} contains an empty string.
* @throws IllegalStateException
* if this multi-subject feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(List)
* @see #addAllFeeds(Pattern)
* @see #closeFeed(String)
* @see #closeAllFeeds(Pattern)
*/
public final void closeAllFeeds(final List subjects)
{
Objects.requireNonNull(subjects, NULL_SUBJECTS);
// Are all the subject non-null and not empty?
if (subjects.contains(null))
{
throw (
new NullPointerException(
SUBJECTS_CONTAINS_NULL));
}
if (subjects.contains(""))
{
throw (
new IllegalArgumentException(
SUBJECTS_CONTAINS_EMPTY_STRING));
}
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// feeds.
throw (new IllegalStateException(FEED_IS_INACTIVE));
}
closeFeeds(subjects);
} // end of closeAllFeeds(List<>)
/**
* Removes and closes all individual feeds whose subject
* matches the given subject query. Does nothing if there are
* no such matching subordinate feed subjects.
* @param query find all subordinate feeds whose subject
* matches this query.
* @throws NullPointerException
* if {@code query} is {@code null}.
* @throws IllegalStateException
* if this multi-subject feed is closed.
*
* @see #addFeed(String)
* @see #addAllFeeds(List)
* @see #addAllFeeds(Pattern)
* @see #closeFeed(String)
* @see #closeAllFeeds(List)
*/
public final void closeAllFeeds(final Pattern query)
{
Objects.requireNonNull(query, NULL_QUERY);
// Is this feed still active?
if (!mIsActive.get())
{
// No. Can't add new feeds to closed multi-subject
// 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()));
closeFeeds(subjects);
} // end of closeAllFeeds(Pattern)
/**
* 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(this::doAddFeed);
} // end of addFeeds(List<>)
/**
* Closes existing feeds for for of the list subjects.
* @param subjects close feeds referenced by these subjects.
*/
private void closeFeeds(final List subjects)
{
subjects.stream()
// Ignore unknown feeds.
.filter(mFeeds::containsKey)
.forEachOrdered(this::doCloseFeed);
} // end of closeFeeds(List<>)
/**
* Performs the actual work of creating a new subordinate
* feed for the given subject. Calling method has already
* verified that this feed is still active and that the
* subject is valid and unique.
* @param subject create subordinate feed for this subject.
*/
private void doAddFeed(final String subject)
{
final F feed =
createFeed(new EMessageKey(mMsgClass, subject));
mFeeds.put(subject, feed);
sLogger.debug(
"{} multi-subject feed {} ({}): adding feed {}.",
mEClient.location(),
mEClient.clientId(),
mScope,
subject);
// Is this multikey feed subscribed?
if (mInPlace)
{
// Yes, put this new feed in place.
putFeedInPlace(feed);
}
} // end of doAddFeed(String)
/**
* Performs the actual work of removing and closing an
* existing subordinate feed for the given subject. Calling
* method has already verified that this feed is still active
* and that the subject is valid and references a known
* subordinate feed.
* @param subject subordinate feed subject.
*/
private void doCloseFeed(final String subject)
{
(mFeeds.remove(subject)).close();
sLogger.debug(
"{} multi-subject feed {} ({}): removed feed {}.",
mEClient.location(),
mEClient.clientId(),
mScope,
subject);
} // end of doCloseFeed(String)
//---------------------------------------------------------------
// Inner classes.
//
/**
* Base class for multiple subject feeds. Defines
* {@link #createFeed(EMessageKey)} which creates a
* subordinate feed for the given message key.
*
* @param all feeds are for this message type.
* @param feed type.
* @param {@link EObject} sub-type.
* @param target multi-feed type.
* @param builder leaf type.
*/
protected abstract static class Builder,
B extends Builder>
extends EFeed.Builder
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Message class must be assignable from this class.
*/
protected final Class mTargetMsgClass;
/**
* Subordinate feeds must be for this message class.
*/
protected Class extends C> mMsgClass;
/**
* Subordinate feeds contained in this multi-feed.
*/
protected final Map mFeeds;
/**
* Defines whether this is a subject list or
* pattern-based multifeed.
*/
private MultiFeedType mMultiFeedType;
/**
* Contains initial subjects list.
*/
private List mSubjects;
/**
* Message subject query used to look up existing
* subjects for a given message class.
*/
private Pattern mQuery;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates a new multi-feed builder for the given target
* feed class.
* @param targetClass target multi-feed class.
* @param targetMsgClass target eBus message class.
*/
protected Builder(final Class targetClass,
final Class targetMsgClass)
{
super (targetClass);
mTargetMsgClass = targetMsgClass;
mMultiFeedType = MultiFeedType.NOT_SET;
mFeeds = new TernarySearchTree<>();
mSubjects = Collections.emptyList();
} // end of Builder(Class, Class)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Returns a subordinate feed for the given message key.
*
* Note: this method is called after the
* builder validation.
*
* @param key subordinate feed's message key.
* @return subordinate feed.
*/
protected abstract F createFeed(final EMessageKey key);
//
// end of Abstract Method Declarations.
//-------------------------------------------------------
//-------------------------------------------------------
// Abstract Method Overrides.
//
/**
* Checks if message class and query (if this is a
* query-based multi-feed) are set.
* @param problems report configuration failures to this
* validator.
* @return {@code problems}.
*/
@Override
protected Validator validate(Validator problems)
{
final boolean localOnlyNotification =
(mMsgClass != null &&
mMsgClass.isAnnotationPresent(
ELocalOnly.class));
super.validate(problems)
.requireNotNull(mMsgClass, "messageClass")
.requireTrue(mMultiFeedType != MultiFeedType.NOT_SET,
"subjects",
"neither subjects or query set")
.requireTrue(((localOnlyNotification &&
mScope == FeedScope.LOCAL_ONLY) ||
!localOnlyNotification),
"messageKey, feedScope",
String.format(
"%s is local-only but feed scope is %s",
mMsgClass,
mScope));
// Were any problems found?
if (problems.isEmpty())
{
// No. Create initial subjects.
// If this is a query-based multi-feed and the
// query is set, then look up the subjects.
if (mMultiFeedType == MultiFeedType.QUERY &&
mQuery != null)
{
mSubjects = findSubjects(mQuery);
}
// Create initial subordindate feeds.
mSubjects.forEach(
s ->
mFeeds.put(
s,
createFeed(
new EMessageKey(mMsgClass, s))));
}
return (problems);
} // end of validate(Validator)
//
// end of Abstract Method Overrides.
//-------------------------------------------------------
//-------------------------------------------------------
// Set Methods.
//
/**
* Sets feed message class. All messages sent or received
* on this multi-feed must be of this class.
* @param mc feed message class.
* @return {@code this Builder} instance.
* @throws NullPointerException
* if {@code mc} is {@code null}.
* @throws IllegalArgumentException
* if {@code mc} is not assignable from target message
* class.
*/
public B messageClass(final Class extends C> mc)
{
mMsgClass =
Objects.requireNonNull(
mc, "message class is null");
return (self());
} // end of messageClass(final Class> mc)
/**
* Sets initial multi-feed subjects.
* @param subjects initial multi-feed subjects.
* @return {@code this Builder} instance.
* @throws IllegalStateException
* if subjects list or subject query is already set.
* @throws IllegalArgumentException
* if {@code subjects} contains a {@code null} or empty
* string value.
*/
public final B subjects(final List subjects)
{
if (mMultiFeedType != MultiFeedType.NOT_SET)
{
throw (
new IllegalStateException(
"initial subjects already set, cannot be set again"));
}
Objects.requireNonNull(subjects, NULL_SUBJECTS);
// Are all the subject non-null and non-empty?
if (subjects.contains(null))
{
throw (
new NullPointerException(
SUBJECTS_CONTAINS_NULL));
}
if (subjects.contains(""))
{
throw (
new IllegalArgumentException(
SUBJECTS_CONTAINS_EMPTY_STRING));
}
// Note subjects list may be empty.
mSubjects = ImmutableList.copyOf(subjects);
mMultiFeedType = MultiFeedType.LIST;
return (self());
} // end of subjects(List<>)
/**
* Sets query for looking up initial multi-feed subjects.
* @param query match existing subjects against this
* query.
* @return {@code this Builder} instance.
* @throws IllegalStateException
* if this is multi-feed uses an initial subject list.
* @throws NullPointerException
* if {@code query} is {@code null}.
*/
public final B query(final Pattern query)
{
if (mMultiFeedType != MultiFeedType.NOT_SET)
{
throw (
new IllegalStateException(
"initial subjects already set, cannot be set again"));
}
mQuery =
Objects.requireNonNull(query, NULL_QUERY);
mMultiFeedType = MultiFeedType.QUERY;
return (self());
} // end of query(Pattern)
//
// end of Set Methods.
//-------------------------------------------------------
/**
* Returns immutable list containing those existing
* subjects matching the given query. Returned list will
* not be {@code null} but may be empty.
* @param query match subjects against this query.
* @return list of matching subjects.
*/
private List findSubjects(final Pattern query)
{
final Pattern keyPattern =
Pattern.compile(mMsgClass.getName() +
EMessageKey.KEY_IFS +
query.pattern());
final ImmutableList.Builder builder =
ImmutableList.builder();
// Extract the subjects from the matching message keys.
ESubject.findKeys(keyPattern)
.forEach(key -> builder.add(key.subject()));
return (builder.build());
} // end of findSubjects(Pattern)
} // end of class Builder
} // end of class EMultiFeed