
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 java.util.Iterator;
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.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;
/**
* {@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 =
Logger.getLogger(ENotifySubject.class.getName());
//-----------------------------------------------------------
// 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 */ synchronized EMessageHeader localAd(final AdStatus adStatus)
{
EMessageHeader retval = null;
if (mPublishers.supports(ClientLocation.REMOTE) > 0)
{
final EFeedState fs =
mPublishers.feedState(ClientLocation.REMOTE);
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 */ synchronized EFeedState advertise(final EPublishFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
final int activationCount =
mSubscribers.isSupportedBy(scope);
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s: adding %s client/%s scope advertiser %d, feed %d.",
mKey,
location,
scope,
feed.clientId(),
feed.feedId()));
}
// Add the feed to the advertisers feed list.
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) &&
mPublishers.supports(FeedScope.REMOTE_ONLY) == 1)
{
final EFeedState fs =
mPublishers.feedState(ClientLocation.REMOTE);
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 */ synchronized void unadvertise(EPublishFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
final int feedCount = mPublishers.remove(feed);
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s: removing %s client/%s scope advertiser %d, feed %d.",
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 */ synchronized void updateFeedState(final EPublishFeed feed)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s: updating %s publisher %d %s to %s.",
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 */ synchronized void publish(final ENotificationMessage msg,
final EPublishFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
final Iterator fit =
mSubscribers.iterator(scope);
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format("%s: %s/%s publisher %d, feed %d message:%n%s",
mKey,
location,
scope,
feed.clientId(),
feed.feedId(),
feed.feedState(),
msg));
}
else if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format("%s: %s/%s publisher %d, feed %d message.",
mKey,
location,
scope,
feed.clientId(),
feed.feedId(),
feed.feedState()));
}
// Post the message to each contra-zone subscribers.
while (fit.hasNext())
{
(fit.next()).notify(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 */ synchronized void subscribe(final ESubscribeFeed f)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"%s: adding %s/%s subscriber %d, feed %d.",
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 */ synchronized void unsubscribe(final ESubscribeFeed feed)
{
final ClientLocation location = feed.location();
final FeedScope scope = feed.scope();
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format("%s: removing %s/%s subscriber %d, feed %d.",
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);
if (sLogger.isLoggable(Level.FINE))
{
sLogger.finest(
String.format(
"%s: 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