package org.marketcetera.core.publisher; import java.util.LinkedHashSet; import java.util.concurrent.*; import org.marketcetera.core.ClassVersion; import org.marketcetera.util.log.SLF4JLoggerProxy; import org.marketcetera.util.misc.NamedThreadFactory; /** * Publication engine which supplies the Publish side of the Publish/Subscribe contract. * * <p>This object is meant to be used by means of a "has-a" relationship. * * @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a> * @version $Id: PublisherEngine.java 16841 2014-02-20 19:59:04Z colin $ * @since 0.5.0 */ @ClassVersion("$Id: PublisherEngine.java 16841 2014-02-20 19:59:04Z colin $") public final class PublisherEngine implements IPublisher { /** * the queue of subscribers - should be maintained in FIFO order */ private final LinkedHashSet<ISubscriber> mSubscribers = new LinkedHashSet<ISubscriber>(); /** * A mirror of {@link #mSubscribers} kept for fast traversal when * publishing events without needing locks . The contents of this array are * never modified. To make any changes, a new copy of this array is created * and the reference is updated to point to the new array. */ private volatile ISubscriber[] mSubscriberArray; /** * the pool of notifiers common to all <code>PublisherEngine</code> objects */ private static final ExecutorService sNotifierPool = Executors.newCachedThreadPool(new NamedThreadFactory("Publisher-")); //$NON-NLS-1$ /** * indicates whether this publisher should do all publications synchronously or not */ private final boolean mSynchronousNotification; /** * Create a new <code>PublisherEngine</code> object. * <p> * The engine can do publications synchronously or asynchronously. * <p> * When doing publications synchronously, all the subscribers are * notified in the same thread as the one invoking * {@link #publish(Object)}. The <code>publish()</code> method does not * return until all the subscribers have been notified. * <p> * When doing publications asynchronously, the subscribers are notified * in a separate thread from the one invoking {@link #publish(Object)}. * In this configuration <code>publish()</code> may return before or * after the subscribers have been notified. Moreover, the subscribers * may receive events in a different order from which they have been * published as the events are received by subscribers in different * threads. * * @param inSynchronousNotification if the publisher engine should * publish events synchronously or not. */ public PublisherEngine(boolean inSynchronousNotification) { mSynchronousNotification = inSynchronousNotification; } /** * Create a new <code>PublisherEngine</code> object. * <p> * Invoking this constructor is the same as invoking * <code>new PublisherEngine(false)</code>. */ public PublisherEngine() { this(false); } /** * Indicates whether the publisher is notifying synchronously. * * @return a <code>PublisherEngine</code> value */ public boolean isSynchronousNotification() { return mSynchronousNotification; } /** * Advertise for publication the given object to all subscribers. * * @param inData an <code>Object</code> value */ public void publish(Object inData) { doPublish(inData); } /** * Advertise for publication the given object to all subscribers and wait * until all publications are done. * * <p> * This method will block until all publications are complete. * <p> * This method is no different from {@link #publish(Object)} if * {@link #isSynchronousNotification()} is true. * * @param inData an <code>Object</code> value * @throws InterruptedException if the thread is interrupted while waiting * for notifications to complete * @throws ExecutionException if the subscriber threw an unexpected error. */ public void publishAndWait(Object inData) throws InterruptedException, ExecutionException { Future<?> future = doPublish(inData); if (future != null) { future.get(); } } /* (non-Javadoc) * @see org.marketcetera.core.publisher.IPublisher#subscribe(org.marketcetera.core.publisher.ISubscriber) */ @Override public void subscribe(ISubscriber inSubscriber) { if(inSubscriber == null) { return; } synchronized(mSubscribers) { // don't have to worry if the subscriber is already present, // according to the LinkedHashSet contract, reinsertion does // not affect order mSubscribers.add(inSubscriber); synchronizeSubscriberArray(); } } /* (non-Javadoc) * @see org.marketcetera.core.publisher.IPublisher#unsubscribe(org.marketcetera.core.publisher.ISubscriber) */ @Override public void unsubscribe(ISubscriber inSubscriber) { if(inSubscriber == null) { return; } synchronized(mSubscribers) { // don't have to worry if the subscriber is not present, // Set takes care of that for us mSubscribers.remove(inSubscriber); synchronizeSubscriberArray(); } } /* (non-Javadoc) * @see org.marketcetera.core.publisher.IPublisher#getSubscriptionCount() */ @Override public int getSubscriptionCount() { return mSubscribers.size(); } /** * Synchronizes the subscriber array with the list of subscribers. * <p> * This method should be invoked whenever subscribers list is modified. * The lock on subscribers list should be acquired before this method * is invoked. */ private void synchronizeSubscriberArray() { mSubscriberArray = mSubscribers.toArray( new ISubscriber[mSubscribers.size()]); } /** * Perform the actual publication to subscribers. * * @param inData an <code>Object</code> value. * * @return a non-null future value if {@link #isSynchronousNotification()} * is true, a null value otherwise. */ private Future<?> doPublish(final Object inData) { final ISubscriber[] subscribers = mSubscriberArray; SLF4JLoggerProxy.debug(this, "Publishing {} to {} subscriber(s)", //$NON-NLS-1$ inData, (subscribers == null ? 0 : subscribers.length)); if(subscribers == null) { return null; } if (isSynchronousNotification()) { publishToSubscribers(subscribers, inData); return null; } else { // hand the notification chore to a thread from the thread pool return sNotifierPool.submit(new Runnable() { public void run() { publishToSubscribers(subscribers, inData); } }); } } /** * Publishes the supplied data object to the specified list of subscribers. * * @param inSubscribers The list of subscribers that need to be notified. * @param inData the data to publish to the subscribers. */ private static void publishToSubscribers(ISubscriber[] inSubscribers, Object inData) { for (ISubscriber subscriber: inSubscribers) { try { if (subscriber.isInteresting(inData)) { subscriber.publishTo(inData); } } catch (Throwable t) { SLF4JLoggerProxy.debug(PublisherEngine.class, t, "Subscriber {} threw an exception during publication, skipping", //$NON-NLS-1$ subscriber); } } } }