net.sf.eBus.client.ENotifySubject Maven / Gradle / Ivy
//
// Copyright 2010 - 2016, 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 net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.sysmessages.AdMessage;
import net.sf.eBus.client.sysmessages.AdMessage.AdStatus;
import net.sf.eBus.client.sysmessages.SystemMessageType;
import net.sf.eBus.messages.EMessage.MessageType;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.ENotificationMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@code ENotifySubject} connects notification publishers with
* subscribers, based on their respective feed scopes.
* A notification feed is up if there is at least one subscriber
* for a specified notification message key. After a publisher
* advertises, {@code ENotifySubject} informs the publisher if
* there are any subscribers in its feed scope or not by
* calling
* {@link EPublisher#publishStatus(EFeedState, EPublishFeed)}
* with the appropriate {@link EFeedState feed state}. The
* publisher should not
* {@link EPublishFeed#publish(ENotificationMessage) publish}
* notification messages when the feed is
* {@link EFeedState#DOWN down} as this results in an
* {@link IllegalStateException}. The subscriber is notified if
* there are any publishers within its subscription feed scope
* via the
* {@link ESubscriber#feedStatus(EFeedState, ESubscribeFeed)}
* callback.
*
* Notice that it is possible for publishers and subscribers for
* the same message key to have different feed states because the
* feeds have a different scope. For example, an application has
* two subscribers to the message key
* {@code com.acme.mgt.SystemStatus:ActiveServer}. One subscriber
* is a local only scope and the other has remote only scope.
* Since the application is not running on the active server but
* the stand-by, the first subscription feed will be down while
* the second feed state will be up (assuming the stand-by server
* is connected to the active server).
*
*
* @see ESubject
* @see EPublisher
* @see ESubscriber
* @see ENotifyFeed
* @see EPublishFeed
* @see ESubscribeFeed
*
* @author Charles Rapp
*/
/* package */ final class ENotifySubject
extends ESubject
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Statics.
//
/**
* The notification subject class logger.
*/
private static final Logger sLogger =
LoggerFactory.getLogger(ENotifySubject.class);
//-----------------------------------------------------------
// Locals.
//
/**
* The currently advertised publish feeds. There is a
* separate list for each feed zone.
*/
private final EFeedList mPublishers;
/**
* The currently subscribed subscribe feeds. There is a
* separate list for each feed zone.
*/
private final EFeedList mSubscribers;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new notification subject for the given unique
* message key.
* @param key the unique notification message key.
*/
/* package */ ENotifySubject(final EMessageKey key)
{
super (key);
mPublishers = new EFeedList<>();
mSubscribers = new EFeedList<>();
} // end of ENotifySubject(EMessageKey)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// ESubject Abstract Method Implementations.
//
/**
* Returns a message header containing a local publisher
* advertisement if any of the local publishers
* support remote publication. Otherwise return {@code null}.
* @param adStatus adding or removing advertisements.
* @return advertisement message or {@code null} if this
* notification feed cannot be remotely advertised.
*/
@Override
/* package */ EMessageHeader localAd(final AdStatus adStatus)
{
final EFeedState fs =
mPublishers.feedState(ClientLocation.REMOTE);
EMessageHeader retval = null;
if (fs != EFeedState.UNKNOWN)
{
retval =
new EMessageHeader(
(SystemMessageType.AD).keyId(),
ERemoteApp.NO_ID,
ERemoteApp.NO_ID,
(AdMessage.builder()).messageKey(mKey)
.adStatus(adStatus)
.adMessageType(MessageType.NOTIFICATION)
.feedState(fs)
.build());
}
return (retval);
} // end of localAd(AdStatus)
//
// end of ESubject Abstract Method Implementations.
//-----------------------------------------------------------
/**
* Adds the given publisher feed to the advertiser feed list
* for the feed's scope.
*
* If this is the first local client/local & remote feed,
* then the advertisement is forwarded to all remote eBus
* applications.
*
* @param feed a publishing feed.
* @return returns the current subscriber feed state.
*/
/* package */ EFeedState advertise(final EPublishFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
final int activationCount =
mSubscribers.isSupportedBy(scope);
final EFeedState fs;
sLogger.debug(
"{}: adding {} client/{} scope advertiser {}, feed {}.",
mKey,
location,
scope,
feed.clientId(),
feed.feedId());
// Add the feed to the advertisers feed list.
fs = mPublishers.add(feed);
// DO NOT INFORM SUBSCRIBERS ABOUT THIS ADVERTISEMENT.
// Wait for the publish state update and then inform
// subscribers. We do not know yet if the publisher feed
// is up.
// If this feed is local client and a remote feed and
// the first one to boot, then forward the advertisement
// to all remote eBus applications currently connected.
// Multicast feeds are *not* forwarded to remote eBus
// applications.
if (location == ClientLocation.LOCAL &&
scope.intersects(FeedScope.REMOTE_ONLY) &&
fs != EFeedState.UNKNOWN)
{
ERemoteApp.forwardAll(
new EMessageHeader(
(SystemMessageType.AD).keyId(),
ERemoteApp.NO_ID,
ERemoteApp.NO_ID,
(AdMessage.builder()).messageKey(mKey)
.adStatus(AdStatus.ADD)
.adMessageType(MessageType.NOTIFICATION)
.feedState(fs)
.build()));
}
// Post the current subscriber activation count to the
// publisher, returning the feed state.
return (feed.updateActivate(activationCount));
} // end of advertise(EPublishFeed)
/**
* Removes the given publisher feed to the advertiser feed
* list. If this is the last local client/local & remote
* feed, then the advertisement is retracted from all remote
* eBus applications.
* @param feed a publishing feed.
*/
/* package */ void unadvertise(EPublishFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
final int feedCount = mPublishers.remove(feed);
sLogger.debug(
"{}: removing {} client/{} scope advertiser {}, feed {}.",
mKey,
location,
scope,
feed.clientId(),
feed.feedId());
// If the feed's publish state is up, then set the feed
// state to down.
if (feed.feedState() == EFeedState.UP)
{
mSubscribers.updateCount(feed, EFeedState.DOWN);
}
// If this feed is local client, remote feed and
// the last one to boot, then forward the unadvertisement
// to all remote eBus applications currently connected.
if (location == ClientLocation.LOCAL &&
(scope == FeedScope.LOCAL_AND_REMOTE ||
scope == FeedScope.REMOTE_ONLY) &&
feedCount == 0)
{
ERemoteApp.forwardAll(
new EMessageHeader(
(SystemMessageType.AD).keyId(),
ERemoteApp.NO_ID,
ERemoteApp.NO_ID,
(AdMessage.builder()).messageKey(mKey)
.adStatus(AdStatus.REMOVE)
.adMessageType(MessageType.NOTIFICATION)
.feedState(EFeedState.DOWN)
.build()));
}
} // end of unadvertise(EPublishFeed)
/**
* Updates the publisher state as contained in
* {@code feed}. If this publish state change results in
* subscriber feed state change, then all interested
* subscribers are informed of this change.
* @param feed update this feed's state.
*/
/* package */ void updateFeedState(final EPublishFeed feed)
{
sLogger.debug("{}: updating {} publisher {} {} to {}.",
mKey,
feed.location(),
feed.clientId(),
feed.key(),
feed.publishState());
mSubscribers.updateCount(feed, feed.publishState());
} // end of updateFeedState(EPublishFeed)
/**
* Forwards the message to all subscriber feeds matching the
* publisher feed zone.
* @param msg post this message to the subscriber feeds.
* @param feed the publisher feed.
*/
/* package */ void publish(final ENotificationMessage msg,
final EPublishFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
if (sLogger.isTraceEnabled())
{
sLogger.trace(
"{}: {}/{} publisher {}, feed {} message:\n{}",
mKey,
location,
scope,
feed.clientId(),
feed.feedId(),
feed.feedState(),
msg);
}
else
{
sLogger.debug(
"{}: {}/{} publisher {}, feed {} message.",
mKey,
location,
scope,
feed.clientId(),
feed.feedId(),
feed.feedState());
}
// Post the message to each contra-zone subscribers.
(mSubscribers.feeds(scope)).forEach(f -> f.notify(msg));
exhaust(msg);
} // end of publish(ENotificiationMessage, EPublishFeed)
/**
* Adds the given subscriber feed to the subscriber feed
* list. Informs publisher feeds if this subscription changes
* their publisher feed state.
* @param f subscriber feed.
*/
/* package */ void subscribe(final ESubscribeFeed f)
{
sLogger.debug("{}: adding {}/{} subscriber {}, feed {}.",
mKey,
f.location(),
f.scope(),
f.clientId(),
f.feedId());
// Add the feed to the subscribers feed list.
mSubscribers.add(f);
// Update the publisher activation count.
f.updateActivate(
mPublishers.updateCount(f, EFeedState.UP));
} // end of subscribe(ESubscribeFeed)
/**
* Removes the given subscriber feed from the subscriber feed
* list. Informs publisher feeds if this retraction changes
* their publisher feed state.
* @param feed subscriber feed.
*/
/* package */ void unsubscribe(final ESubscribeFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
sLogger.debug(
"{}: removing {}/{} subscriber {}, feed {}.",
mKey,
location,
scope,
feed.clientId(),
feed.feedId());
// Remove the feed from the subscribes feed list.
mSubscribers.remove(feed);
// Update the publish activation count.
mPublishers.updateCount(feed, EFeedState.DOWN);
} // end of unsubscribe(ESubscribeFeed)
/**
* Returns the notification subject for the given message
* key. If this subject does not already exist, then creates
* the subject.
*
* The caller is expected to have verified that {@code key}
* is a non-{@code null} reference to a notification message.
* This method does not validate {@code key}.
*
* @param key notification message key.
* @return the notification subject for the given message
* key.
* @throws IllegalArgumentException
* if {@code key} is either {@code null} or not a
* notification message key.
*/
@SuppressWarnings ("unchecked")
/* package */ static ENotifySubject findOrCreate(final EMessageKey key)
{
final ENotifySubject retval;
synchronized (sSubjects)
{
retval = doFindOrCreate(key);
}
return (retval);
} // end of findOrCreate(EMessageKey)
/**
* Performs the actual work of finding or creating a
* notification message subject. Caller is expected to
* {@code syncronized (sSubjects)} prior to calling this
* method.
* @param key find or create this notification subject.
* @return notification subject.
*/
/* package */ static ENotifySubject doFindOrCreate(final EMessageKey key)
{
final String keyString = key.keyString();
ENotifySubject retval =
(ENotifySubject) sSubjects.get(keyString);
// Do we need to open the subject?
if (retval == null)
{
// Yes. Do so and store it away in the subjects
// tree.
retval = new ENotifySubject(key);
sSubjects.put(keyString, retval);
sLogger.trace(
"{}: created notification subject.", key);
// Let listeners know about this new notification
// subject.
ESubject.forwardUpdate(
SubjectType.NOTIFICATION, key);
}
return (retval);
} // end of doFindOrCreate(EMessageKey)
} // end of class ENotifySubject