
net.sf.eBus.client.ESubject Maven / Gradle / Ivy
//
// Copyright 2010, 2011, 2013-2016, 2020, 2022 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.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import net.sf.eBus.client.EClient.ClientLocation;
import net.sf.eBus.client.sysmessages.AdMessage;
import net.sf.eBus.client.sysmessages.AdMessage.AdStatus;
import net.sf.eBus.logging.AsyncLoggerFactory;
import net.sf.eBus.messages.EMessage;
import net.sf.eBus.messages.EMessageHeader;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.util.TernarySearchTree;
import net.sf.eBus.util.logging.StatusReporter;
import net.sf.eBus.util.regex.Pattern;
import org.slf4j.Logger;
/**
* The eBus subject performs the actual work of connecting
* application instances together via their respective
* {@link EFeed feeds}. {@code ESubject} instances are
* responsible for connecting {@link EPublisher publishers} to
* {@link ESubscriber subscribers} and {@link EReplier repliers}
* with {@link ERequestor requestors}. There is one subject
* instance for each unique {@link EMessageKey message key}.
*
* This class is responsible for maintaining a static ternary
* search tree mapping a
* {@link EMessageKey#keyString() key string} to the eBus subject
* instance for that message key.
*
*
* The eBus API is intended to be extended with new feed and
* subject classes. 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.
*
*
* @see ENotifySubject
* @see ERequestSubject
* @see EFeed
*
* @author Charles Rapp
*/
@SuppressWarnings ("unchecked")
/* package */ abstract class ESubject
implements Comparable
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Statics.
//
/**
* Maps the {@link EMessageKey} to its eBus subject.
* {@link EMessageKey#keyString} is used for the mapping.
*/
protected static final TernarySearchTree sSubjects =
new TernarySearchTree<>();
/**
* Local {@code EClient} instance used to dispatch exhaust
* tasks.
*/
private static final EClient sExhauster =
EClient.findOrCreateClient(
new EObject() { }, ClientLocation.LOCAL);
/**
* Contains instance used to exhaust messages passing through
* eBus to persistent store. Initialized to default exhaust
* which does nothing with the given message.
*/
private static final AtomicReference sExhaust =
new AtomicReference<>(ESubject::defaultExhaust);
/**
* Forward newly added subject's type and key to these
* listeners.
*/
private static final List sSubjectListeners =
new ArrayList<>();
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
AsyncLoggerFactory.getLogger(ESubject.class);
//-----------------------------------------------------------
// Locals.
//
/**
* The subject message key.
*/
protected final EMessageKey mKey;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new subject instance for the given message key.
* @param key the unique subject message key.
*/
protected ESubject(final EMessageKey key)
{
mKey = key;
} // end of ESubject(EMessageKey)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Abstract Method Declarations.
//
/**
* Returns a non-{@code null} {@link AdMessage} if this
* subject currently has a local feed which supports a remote
* client. Note that the feed state does not need to be
* {@link EFeedState#UP}, only advertised. If no such feed
* exists, then returns {@code null}.
* @param adStatus either add or remote this advertisement.
* @return either an {@code AdMessage} or {@code null}.
*/
/* package */ abstract EMessageHeader localAd(final AdStatus adStatus);
//
// end of Abstract Method Declarations.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Comparable Interface Implementation.
//
/**
* Returns an integer value <, equal to or > zero
* depending on whether {@code this ESubject}
* instance is <, equal to or > {@code subject}.
* The comparison is based on
* {@link net.sf.eBus.messages.EMessageKey}.
* @param subject comparison instance.
* @return an integer value <, equal to or > zero
* depending on whether {@code this ESubject}
* instance is <, equal to or > {@code subject}.
*/
@Override
public int compareTo(final ESubject subject)
{
return (mKey.compareTo(subject.mKey));
} // end of compareTo(ESubject)
//
// end of Comparable Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Object Method Overrides.
//
/**
* Returns {@code true} if {@code o} is a
* non-{@code null ESubject} whose message key equals
* {@code this} subject; otherwise returns {@code false}.
* @param o comparison object.
* @return {@code true} if {@code o} message keys equals
* {@code this} message key.
*/
@Override
public boolean equals(final Object o)
{
boolean retcode = (this == o);
if (!retcode && o instanceof ESubject)
{
retcode = mKey.equals(((ESubject) o).mKey);
}
return (retcode);
} // end of equals(Object)
/**
* Returns the message key hash code.
* @return the hash code for this subject instance.
*/
@Override
public int hashCode()
{
return (mKey.hashCode());
} // end of hashCode()
/**
* Returns the message key text representation.
* @return the message key text representation.
*/
@Override
public String toString()
{
return (mKey.toString());
} // end of toString()
//
// end of Object Method Overrides.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns this subject message key.
* @return this subject message key.
*/
public final EMessageKey key()
{
return (mKey);
} // end of key()
/**
* Returns a new subject status reporter instance.
* @return subject status reporter instance.
*/
public static SubjectStatusReporter getStatusReporter()
{
return (new SubjectStatusReporter());
} // end of getStatusReporter()
/**
* Returns a non-{@code null}, possibly empty, message key
* list taken from the current message key dictionary
* entries.
* @return mutable list message key dictionary entries.
*/
/* package */ static List findKeys()
{
final Collection subjects;
final ImmutableList.Builder retval =
ImmutableList.builder();
synchronized (sSubjects)
{
subjects = sSubjects.values();
}
// Put the subject message keys into the returned list.
subjects.forEach(subject -> retval.add(subject.key()));
return (retval.build());
} // end of findKeys()
/**
* Returns a non-{@code null}, possibly empty, list of
* message keys from the message key dictionary matching the
* given regular express query.
* @param query match against this pattern.
* @return list of message key dictionary entries matching
* {@code query}.
*/
/* package */ static List findKeys(final Pattern query)
{
final Collection subjects;
final ImmutableList.Builder retval =
ImmutableList.builder();
synchronized (sSubjects)
{
subjects = sSubjects.values(query);
}
// Put the subject message keys into the returned list.
subjects.forEach(subject -> retval.add(subject.key()));
return (retval.build());
} // end of findKeys(Pattern)
/**
* Returns a list of local advertisements which
* support a remote client. If subject returns a
* non-{@code null} message and {@code adStatus} is
* {@link AdStatus#ADD}, then put the subject's feed state
* message on to the list as well.
* @param adStatus either add or remove this advertisement.
* @return advertise and feed state message list.
*/
/* package */ static List localAds(final AdStatus adStatus)
{
EMessageHeader msg;
final ImmutableList.Builder retval =
ImmutableList.builder();
// For each subject, get its ad message.
for (ESubject subject : sSubjects.values())
{
msg = subject.localAd(adStatus);
if (msg != null)
{
retval.add(msg);
}
}
return (retval.build());
} // end of localAds(AdStatus)
//
// end of Get Methods.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Set Methods.
//
/**
* 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.
*/
/* package */ static void setExhaust(final IMessageExhaust exhaust)
{
if (exhaust == null)
{
sExhaust.set(ESubject::defaultExhaust);
}
else
{
sExhaust.set(exhaust);
}
} // end of setExhaust(IMessageExhaust)
/**
* Adds the given subject listener to the listeners list.
* Does nothing if the listener is already registered.
* @param l add this subject listener.
*/
/* package */ static void addListener(final ISubjectListener l)
{
final EClient eClient =
EClient.findOrCreateClient(l, ClientLocation.LOCAL);
synchronized (sSubjectListeners)
{
if (!sSubjectListeners.contains(eClient))
{
sSubjectListeners.add(eClient);
}
}
} // end of addListener(ISubjectListener)
/**
* Removes subject listener from the listeners list.
* @param l remove this subject listener.
*/
/* package */ static void removeListener(final ISubjectListener l)
{
removeListener(EClient.findClient(l));
} // end of removeListener(ISubjectListener)
/**
* Remove subject listener client from the listeners list.
* @param eClient subject listener.
*/
/* package */ static void removeListener(final EClient eClient)
{
if (eClient != null)
{
synchronized (sSubjectListeners)
{
sSubjectListeners.remove(eClient);
}
}
} // end of removeListener(eClient)
/**
* Adds the given message key to the subject tree if not
* already in the tree.
*
* Argument validated prior to this call.
*
* @param key add this message key to subject tree.
*/
/* package */ static void addSubject(final EMessageKey key)
{
synchronized (sSubjects)
{
doAddSubject(key);
}
} // end of addSubject(EMessageKey)
/**
* Adds all the given message keys to the message key tree
* but only if the key is not already in the tree.
*
* Argument validated prior to this call.
*
* @param keys add these message keys to subject tree.
*/
/* package */ static void addAllSubjects(final Collection keys)
{
synchronized (sSubjects)
{
keys.forEach(key -> doAddSubject(key));
}
} // end of addAllSubjects(Collection<>)
//
// end of Set Methods.
//-----------------------------------------------------------
/**
* Writes the entire message key dictionary to the given
* object output stream. The caller is responsible for
* opening and closing 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.
*
* @param oos load message keys to this object output stream.
* @throws IOException
* if an error occurs writing message keys to {@code oos}.
*
* @see #storeKeys(Pattern, ObjectOutputStream)
* @see #loadKeys(ObjectInputStream)
*/
/* package */ static void storeKeys(final ObjectOutputStream oos)
throws IOException
{
final Collection keys;
synchronized (sSubjects)
{
keys = sSubjects.values();
}
storeKeys(keys, oos);
} // end of storeKeys(ObjectOutputStream)
/**
* Writes those message keys matching the given query to
* the object output stream.
* @param query message key regular expression pattern.
* @param oos object output stream.
* @throws IOException
* if an error occurs writing message keys to {@code oos}.
*/
/* package */static void storeKeys(final Pattern query,
final ObjectOutputStream oos)
throws IOException
{
final Collection keys;
synchronized (sSubjects)
{
keys = sSubjects.values(query);
}
storeKeys(keys, oos);
} // end of storeKeys(Pattern, ObjectOutputStream)
/**
* Loads the message keys extracted from the object input
* stream back into the eBus message key dictionary. Note:
* existing message keys are not overwritten or replaced by
* the keys found in {@code ois}.
* @param ois read in message keys from this object input
* stream.
* @throws IOException
* if an error occurs reading in message keys.
*/
/* package */ static void loadKeys(final ObjectInputStream ois)
throws IOException
{
final int numKeys = ois.readInt();
int i;
synchronized (sSubjects)
{
for (i = 0; i < numKeys; ++i)
{
try
{
doAddSubject((EMessageKey) ois.readObject());
}
catch (ClassNotFoundException classex)
{
throw (
new IOException(
"ois contains a non-EMessageKey object",
classex));
}
}
}
} // end of loadKeys(ObjectInputStream)
/**
* Post the specified new subject update to all registered
* listeners.
* @param type new subject's type (notification or request).
* @param key new subject's message key.
*/
protected static void forwardUpdate(final SubjectType type,
final EMessageKey key)
{
synchronized (sSubjectListeners)
{
sSubjectListeners.forEach(
l -> l.dispatch(new SubjectTask(l, type, key)));
}
} // end of forwardUpdate(SubjectType, EMessageKey)
/**
* Dispatches the message and current exhaust via the
* exhauster object.
* @param message exhaust this message.
*/
protected static void exhaust(final EMessage message)
{
sExhauster.dispatch(
new ExhaustTask(message, sExhaust.get()));
} // end of exhaust(EMessage)
/**
* Performs the actual work of writing message keys to the
* object output stream.
* @param subjects write subject keys to {@code oos}.
* @param oos output message keys to this object output
* stream.
* @throws IOException
* if an error occurs writing the message keys to the object
* output stream.
*/
private static void storeKeys(final Collection subjects,
final ObjectOutputStream oos)
throws IOException
{
// Store the number of message key entries first.
oos.writeInt(subjects.size());
for (ESubject subject : subjects)
{
// Now write out each message key.
oos.writeObject(subject.key());
}
} // end of storeKeys(Collection<>)
/**
* Performs the actual work of adding the given message key
* to the subjects map.
* @param key add this message key to the map if not already
* in the map.
*
* @see #addSubject(EMessageKey)
* @see #addAllSubjects(Collection)
* @see #loadKeys(ObjectInputStream)
*/
private static void doAddSubject(final EMessageKey key)
{
final String keyString = key.keyString();
if (!sSubjects.containsKey(keyString))
{
// Is this a notification message?
if (key.isNotification())
{
// Yes. Then create a notify subject.
sSubjects.put(
keyString,
ENotifySubject.doFindOrCreate(key));
}
// Otherwise this a request message. The caller
// has already determined that key is either a
// notification or request.
else
{
// Yes. Then create a request subject.
sSubjects.put(
keyString,
ERequestSubject.doFindOrCreate(key));
}
}
} // end of doAddSubject(EMessageKey)
/**
* Default exhaust does nothing with the message.
* @param message exhaust this message.
*/
private static void defaultExhaust(final EMessage message)
{}
//---------------------------------------------------------------
// Inner classes.
//
/**
* Forwards subject update to eBus client implementing
* {@link ISubjectListener} interface.
*/
// This private inner class is used in forwardUpdate().
@SuppressWarnings({"java:S3985"})
private static final class SubjectTask
implements Runnable
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
private final EClient mEClient;
private final SubjectType mType;
private final EMessageKey mKey;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
private SubjectTask(final EClient eClient,
final SubjectType type,
final EMessageKey key)
{
mEClient = eClient;
mType = type;
mKey = key;
} // end of SubjectTask(EClient,SubjectType,EMessageKey)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementation.
//
@Override
public void run()
{
final Object target = mEClient.target();
// Is the target listener still alive?
if (target != null)
{
try
{
final ISubjectListener l =
(ISubjectListener) target;
l.subjectUpdate(mType, mKey);
}
catch (Exception jex)
{
sLogger.warn(
"SubjectTask[{}, {}, {}] exception",
(target.getClass()).getName(),
mType,
mKey,
jex);
}
}
} // end of run()
//
// end of Runnable Interface Implementation.
//-------------------------------------------------------
} // end of class SubjectTask
/**
* Task used to forward message to exhaust via the
* {@code EClient} dispatch thread.
*/
private static final class ExhaustTask
implements Runnable
{
//-----------------------------------------------------------
// Member data.
//
//-------------------------------------------------------
// Locals.
//
/**
* Exhaust this message to persistent store.
*/
private final EMessage mMessage;
/**
* Message is persisted using this exhaust.
*/
private final IMessageExhaust mExhaust;
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// Constructors.
//
/**
* Creates an exhaust task for the given message and
* exhaust.
* @param message persist this message.
* @param exhaust use this exhaust to persist message.
*/
private ExhaustTask(final EMessage message,
final IMessageExhaust exhaust)
{
mMessage = message;
mExhaust = exhaust;
} // end of ExhaustTask(EMessage, IMessageExhaust)
//
// end of Constructors.
//-------------------------------------------------------
//-------------------------------------------------------
// Runnable Interface Implementation.
//
/**
* Exhausts given message using {@code IMessageExhaust}
* instance. Captures any exception thrown by message
* exhaust and logs as warning.
*/
@Override
public void run()
{
try
{
sLogger.trace("Exhausting {} message.",
mMessage.key());
mExhaust.exhaust(mMessage);
}
catch (Exception jex)
{
sLogger.warn("Exception exhausting {} message",
mMessage.key(),
jex);
}
} // end of run()
//
// end of Runnable Interface Implementation.
//-------------------------------------------------------
} // end of class ExhaustTask
/**
* Adds the subject information to the periodic status
* report for all extant subjects.
*/
private static final class SubjectStatusReporter
implements StatusReporter
{
//-----------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Member methods.
//
//-------------------------------------------------------
// StatusReporter Interface Implementation.
//
/**
* Appends the client and subject count to the periodic
* status report.
* @param report periodic status report.
*/
@Override
public void reportStatus(final PrintWriter report)
{
final int clientCount = EClient.clientCount();
final int subjectCount = sSubjects.size();
report.println();
report.println("EClient:");
report.format(" clients: %,d%n", clientCount);
report.println();
report.println("eBus Subject:");
report.format(" subjects: %,d%n", subjectCount);
} // end of reportStatus(PrintWriter)
//
// end of StatusReporter Interface Implementation.
//-------------------------------------------------------
} // end of class SubjectStatusReporter
} // end of class ESubject