alma.acs.nc.NCSubscriber.java Source code

Java tutorial

Introduction

Here is the source code for alma.acs.nc.NCSubscriber.java

Source

/*
 * ALMA - Atacama Large Millimiter Array
 * (c) European Southern Observatory, 2009 
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 */

package alma.acs.nc;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.scxml.ErrorReporter;
import org.apache.commons.scxml.EventDispatcher;
import org.apache.commons.scxml.SCInstance;
import org.apache.commons.scxml.TriggerEvent;
import org.omg.CORBA.BAD_PARAM;
import org.omg.CORBA.IntHolder;
import org.omg.CORBA.NO_IMPLEMENT;
import org.omg.CORBA.portable.IDLEntity;
import org.omg.CosEventComm.Disconnected;
import org.omg.CosNaming.NamingContext;
import org.omg.CosNotification.EventType;
import org.omg.CosNotification.StructuredEvent;
import org.omg.CosNotification.UnsupportedAdmin;
import org.omg.CosNotification.UnsupportedQoS;
import org.omg.CosNotifyChannelAdmin.AdminLimitExceeded;
import org.omg.CosNotifyChannelAdmin.AdminNotFound;
import org.omg.CosNotifyChannelAdmin.ClientType;
import org.omg.CosNotifyChannelAdmin.InterFilterGroupOperator;
import org.omg.CosNotifyChannelAdmin.ProxyNotFound;
import org.omg.CosNotifyChannelAdmin.ProxySupplier;
import org.omg.CosNotifyChannelAdmin.ProxyType;
import org.omg.CosNotifyChannelAdmin.StructuredProxyPushSupplier;
import org.omg.CosNotifyChannelAdmin.StructuredProxyPushSupplierHelper;
import org.omg.CosNotifyComm.InvalidEventType;
import org.omg.CosNotifyFilter.ConstraintExp;
import org.omg.CosNotifyFilter.Filter;
import org.omg.CosNotifyFilter.FilterFactory;
import org.omg.CosNotifyFilter.FilterNotFound;

import gov.sandia.NotifyMonitoringExt.ConsumerAdmin;
import gov.sandia.NotifyMonitoringExt.ConsumerAdminHelper;
import gov.sandia.NotifyMonitoringExt.EventChannel;
import gov.sandia.NotifyMonitoringExt.EventChannelFactory;
import gov.sandia.NotifyMonitoringExt.NameAlreadyUsed;
import gov.sandia.NotifyMonitoringExt.NameMapError;

import alma.ACSErrTypeCORBA.wrappers.AcsJNarrowFailedEx;
import alma.ACSErrTypeCommon.wrappers.AcsJBadParameterEx;
import alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx;
import alma.ACSErrTypeCommon.wrappers.AcsJIllegalArgumentEx;
import alma.ACSErrTypeCommon.wrappers.AcsJIllegalStateEventEx;
import alma.ACSErrTypeCommon.wrappers.AcsJStateMachineActionEx;
import alma.AcsNCTraceLog.LOG_NC_ConsumerAdminObtained_OK;
import alma.AcsNCTraceLog.LOG_NC_ConsumerAdmin_Overloaded;
import alma.AcsNCTraceLog.LOG_NC_EventReceive_FAIL;
import alma.AcsNCTraceLog.LOG_NC_EventReceive_HandlerException;
import alma.AcsNCTraceLog.LOG_NC_EventReceive_NoHandler;
import alma.AcsNCTraceLog.LOG_NC_EventReceive_OK;
import alma.AcsNCTraceLog.LOG_NC_ProcessingTimeExceeded;
import alma.AcsNCTraceLog.LOG_NC_ReceiverTooSlow;
import alma.AcsNCTraceLog.LOG_NC_SubscriptionConnect_FAIL;
import alma.AcsNCTraceLog.LOG_NC_SubscriptionConnect_OK;
import alma.AcsNCTraceLog.LOG_NC_SupplierProxyCreation_FAIL;
import alma.AcsNCTraceLog.LOG_NC_SupplierProxyCreation_OK;
import alma.AcsNCTraceLog.LOG_NC_TaoExtensionsSubtypeMissing;
import alma.JavaContainerError.wrappers.AcsJContainerServicesEx;
import alma.acs.container.ContainerServicesBase;
import alma.acs.exceptions.AcsJException;
import alma.acs.logging.AcsLogLevel;
import alma.acs.ncconfig.EventDescriptor;
import alma.acsErrTypeLifeCycle.wrappers.AcsJEventSubscriptionEx;
import alma.acsnc.EventDescription;
import alma.acsnc.EventDescriptionHelper;
import alma.acsnc.OSPushConsumer;
import alma.acsnc.OSPushConsumerHelper;
import alma.acsnc.OSPushConsumerOperations;
import alma.acsnc.OSPushConsumerPOA;
import alma.acsnc.OSPushConsumerPOATie;

/**
 * NCSubscriber is the Java implementation of the Notification Channel subscriber,
 * while following the more generic {@link AcsEventSubscriber} interface.
 * <p>
 * This class is used to receive events asynchronously from notification channel suppliers. 
 * It is the replacement of {@link alma.acs.nc.Consumer}, and to keep things simple it no longer
 * supports the inheritance mode, but instead supports type-safe delegation of incoming calls to
 * a user-supplied handler.
 * <p>
 * The lifecycle steps are:
 * <ul>
 *   <li>During creation of an NCSubscriber, the NC and consumer admin  objects are either created or reused,
 *        and a proxy supplier object is created, all inside the notify service. <br>
 *        The reason for creating the proxy supplier (and the other objects along) already at this stage is 
 *        to support event filtering on the server side, with {@link Filter} objects getting attached to the 
 *        proxy supplier, see {@link #addSubscription(alma.acs.nc.AcsEventSubscriber.Callback)}. <br>
 *        TODO: This implementation could be changed to create the server-side filters on demand (e.g. in startReceivingEvents),
 *        so that addSubscription only stores the event type information without yet creating the filter.
 *   <li>Handlers for specialized events ({@link #addSubscription(alma.acs.nc.AcsEventSubscriber.Callback)})
 *       and/or for all events ({@link #addGenericSubscription(alma.acs.nc.AcsEventSubscriber.GenericCallback)}) can be registered.
 *   <li>Once {@link #startReceivingEvents()} is called, Corba NCs push events to this class, which delegates 
 *       the events to the registered handlers.
 *   <li>The connection can then be suspended or resumed.
 * </ul> 
 * The NCSubscriber gets created (and cleaned up if needed) through the container services. 
 * Note about refactoring: NCSubscriber gets instantiated in module jcont using java reflection.
 * Thus if you change the package, name, or constructor of this class, make sure to fix the corresponding "forName" call in jcont.
 * 
 * @param <T>  See base class.
 * 
 * @author jslopez, hsommer, rtobar
 */
public class NCSubscriber<T extends IDLEntity> extends AcsEventSubscriberImplBase<T>
        implements OSPushConsumerOperations, ReconnectableParticipant {

    /**
     * The default maximum amount of time an event handler is given to process
     * event before a warning message is logged. The time unit is floating point seconds.
     * Here we cache the default value defined in EventChannel.xsd, using an XSD binding class.
     */
    private static final double DEFAULT_MAX_PROCESS_TIME_SECONDS = (new EventDescriptor()).getMaxProcessTime();

    /** Provides access to the notify service and CDB, creates NCs, etc */
    protected final Helper helper;

    /**
     * There can be only one notification channel for any given subscriber.
     * The NC is created on demand. 
     * Already in the constructor of this class, the NC's admin object and proxy supplier objects are created or reused. 
     */
    protected EventChannel channel;

    /** The channel has exactly one name registered in the CORBA Naming Service. */
    protected final String channelName;

    /** The channel notification service domain name, can be <code>null</code>. */
    protected final String channelNotifyServiceDomainName;

    /**
     * The consumer admin object attached to the NC,
     * which is used by subscribers to get a reference to the structured supplier proxy.
     * This reference is <code>null</code> when the subscriber is not connected to a NC.
     * <p>
     * The TAO extensions would allow us to set a meaningful name for the admin object, but it 
     * still does not get used as the ID, but as a separate name field.
     * You can get the consumer admin object ID from here, see {@link ConsumerAdmin#MyID()}. 
     * (In the NC spec, it says "The MyID attribute is a readonly attribute that maintains 
     * the unique identifier of the target ConsumerAdmin instance, which is assigned to it 
     * upon creation by the Notification Service event channel.) It is an integer type, which makes 
     * it necessarily different from the name used with the TAO extensions.
     * <p>
     * We try to reuse an admin object for a limited number of subscribers, 
     * to not allocate a new thread in the notification service for every subscriber
     * but instead get a flexible thread::subscriber mapping.
     * 
     * @see #PROXIES_PER_ADMIN
     */
    protected ConsumerAdmin sharedConsumerAdmin;

    /**
     * Maximum number of proxies (subscribers) per admin object.
     * @see #sharedConsumerAdmin
     */
    protected static final int PROXIES_PER_ADMIN = 5;

    /** 
     * The supplier proxy we are connected to. 
     */
    protected StructuredProxyPushSupplier proxySupplier;

    /**
     * The tie poa wrapped around this object, so that we can receive event data over corba.
     */
    private OSPushConsumerPOA corbaObj;

    /**
     * Like {@link #corbaObj}, but activated.
     */
    private OSPushConsumer corbaRef;

    /** 
     * Helper class used to manipulate CORBA anys. 
     */
    protected AnyAide anyAide;

    /** 
     * Whether receiving events should be logged. 
     */
    private final boolean isTraceNCEventsEnabled;

    /**
     * Maps event names to the maximum amount of time allowed for receiver
     * methods to complete. Time is given in floating point seconds.
     */
    protected final HashMap<String, Double> handlerTimeoutMap;

    /**
     * Contains a list of the added and removed subscription filters applied.
     * Key = Event type name (Class#getSimpleName())
     * Value = Filter ID (assigned by the NC)
     */
    protected final Map<String, Integer> subscriptionsFilters = new HashMap<String, Integer>();

    /**
     * Supports reconnection after service restart, see TAO's "topology persistence" extension.
     * @see #reconnect(EventChannelFactory)
     * @see NotifyExt.ReconnectionCallbackOperations
     */
    private AcsNcReconnectionCallback channelReconnectionCallback;

    /**
     * We log it only once if {@link #push_structured_event_called(StructuredEvent)}
     * vetoes down the regular event processing by this NCSubscriber.
     */
    private boolean firstSubclassVeto = true;

    /**
     * To be used only for unit tests.
     */
    private volatile NoEventReceiverListener noEventReceiverListener;

    /**
     * Creates a new instance of NCSubscriber.
     * Normally an ACS class such as container services will act as the factory for NCSubscriber objects, 
     * but for exceptional cases it is also possible to create one stand-alone, 
     * as long as the required parameters can be provided.
     * 
     * @param channelName
     *            Subscribe to events on this channel registered in the CORBA
     *            Naming Service. If the channel does not exist, it's
     *            registered.
     * @param channelNotifyServiceDomainName
     *            Channel domain name, which is being used to determine the 
     *            notification service that should host the NC.
     *            Passing <code>null</code> results in the default notify service "NotifyEventChannelFactory" being used.
     * @param services
     *            To get ACS logger, access to the CDB, etc.
     * @param namingService
     *            Must be passed explicitly, instead of the old hidden approach via <code>ORBInitRef.NameService</code> property.
     * @param clientName
     * @param eventType Our type parameter, either <code>IDLEntity</code> as base type or a concrete IDL-defined struct.
     * @throws AcsJException
     *             Thrown on any <I>really bad</I> error conditions encountered.
     */
    public NCSubscriber(String channelName, String channelNotifyServiceDomainName, ContainerServicesBase services,
            NamingContext namingService, String clientName, Class<T> eventType) throws AcsJException {

        super(services, clientName, eventType);

        // This class will be instantiated through reflection, with an ugly cast,
        // so that in spite of the declaration "NCSubscriber<T extends IDLEntity>" we must verify that eventType is an IDLEntity.
        if (!IDLEntity.class.isAssignableFrom(eventType)) {
            AcsJBadParameterEx ex = new AcsJBadParameterEx();
            ex.setParameter("eventType");
            ex.setParameterValue(eventType.getName());
            ex.setReason("For NCSubscriber, 'eventType' must be (a subtype of) IDLEntity.");
            throw ex;
        }

        if (channelName == null) {
            AcsJBadParameterEx ex = new AcsJBadParameterEx();
            ex.setParameter("channelName");
            ex.setParameterValue("null");
            throw ex;
        }

        if (namingService == null) {
            AcsJBadParameterEx ex = new AcsJBadParameterEx();
            ex.setParameter("namingService");
            ex.setParameterValue("null");
            throw ex;
        }

        this.channelName = channelName;
        this.channelNotifyServiceDomainName = channelNotifyServiceDomainName;

        anyAide = new AnyAide(services);
        helper = new Helper(channelName, channelNotifyServiceDomainName, services, namingService);

        // populate the map with the maxProcessTime an event receiver processing should take
        handlerTimeoutMap = helper.getEventHandlerTimeoutMap();

        isTraceNCEventsEnabled = helper.getChannelProperties().isTraceEventsEnabled(this.channelName);

        // this call is mandatory, see base class ctor comment.
        // It will lead to a call to 'EnvironmentActionHandler#create', 
        // see 'createEnvironmentAction' below.
        stateMachineSignalDispatcher.setUpEnvironment();
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////// State machine actions //////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    protected void createEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        super.createEnvironmentAction(evtDispatcher, errRep, scInstance, derivedEvents);
        try {

            // get the channel
            channel = helper.getNotificationChannel(getNotificationFactoryName());

            // get the admin object

            // Note that admin creation and proxy supplier creation are not synchronized across subscribers,
            // which means that concurrent creation of subscribers can lead to race conditions
            // where we end up with too many (> PROXIES_PER_ADMIN) subscribers
            // for the same admin object.
            // It would be easy to put a static lock around these two calls, which would take care of 
            // concurrent subscribers from the same component or client. Still there would be the same 
            // racing issues coming from distributed subscribers.
            // We prefer to not even do local synchronization because then even in simple unit tests
            // from a single process we can verify the concurrency behavior of subscribers and notifyService.
            // TODO: Revisit the "synchronized(NCSubscriber.class)" block we currently have inside getSharedAdmin(),
            // which is giving partial local synchronization, leading to fewer race conditions.
            // Probably should be removed, or pulled up here and extended around createProxySupplier.
            sharedConsumerAdmin = getSharedAdmin();

            // get the proxy Supplier
            proxySupplier = createProxySupplier();

            // Just check if our shared consumer admin is handling more proxies than it should, and log it
            // (11) goes for the dummy proxy that we're using the transition between old and new NC classes
            int currentProxies = sharedConsumerAdmin.push_suppliers().length - 1;
            if (currentProxies > PROXIES_PER_ADMIN) {
                LOG_NC_ConsumerAdmin_Overloaded.log(logger, sharedConsumerAdmin.MyID(), currentProxies,
                        PROXIES_PER_ADMIN, channelName,
                        channelNotifyServiceDomainName == null ? "none" : channelNotifyServiceDomainName);
            }

            // The user might create this object, and later call startReceivingEvents(), without attaching any receiver.
            // If so, it's useless to get all the events, so we start with an all-exclusive filter in the server
            discardAllEvents();
            //      } catch (OBJECT_NOT_EXIST ex) {
            //         TODO handle dangling NC binding in the naming service (after notify service restart)
        } catch (Throwable thr) {
            throw new AcsJStateMachineActionEx(thr);
        }
    }

    protected void destroyEnvironmentAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        try {
            if (proxySupplier != null) {
                // spec 3.3.10.1: "The disconnect_structured_push_supplier operation is invoked to terminate a
                // connection between the target StructuredPushSupplier and its associated consumer.
                // This operation takes no input parameters and returns no values. The result of this
                // operation is that the target StructuredPushSupplier will release all resources it had
                // allocated to support the connection, and dispose its own object reference."
                proxySupplier.disconnect_structured_push_supplier();
                proxySupplier = null;
                logger.finer("Disconnected and destroyed the supplier proxy");
            }
        } catch (org.omg.CORBA.OBJECT_NOT_EXIST ex1) {
            // This is unexpected but OK, because someone else has already destroyed the remote resources
            logger.fine("No need to release resources for channel " + channelName
                    + " because the NC has been destroyed already.");
        } finally {
            // TODO: Should we not try to destroy an empty consumer admin object? 
            // Or is it too risky because of possible race conditions with newly created other subscribers,
            // given that we don't have a clear distributed locking strategy?
            sharedConsumerAdmin = null;
            channel = null;
        }

        super.destroyEnvironmentAction(evtDispatcher, errRep, scInstance, derivedEvents);
    }

    protected void createConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        super.createConnectionAction(evtDispatcher, errRep, scInstance, derivedEvents);

        try {
            // Register callback for subscribed events
            if (corbaRef == null) {
                corbaObj = new OSPushConsumerPOATie(NCSubscriber.this);
                corbaRef = OSPushConsumerHelper.narrow(helper.getContainerServices().activateOffShoot(corbaObj));
            }
            // Register callback for reconnection requests
            channelReconnectionCallback = new AcsNcReconnectionCallback(NCSubscriber.this, logger);
            channelReconnectionCallback.registerForReconnect(services, helper.getNotifyFactory()); // if the factory is null, the reconnection callback is not registered

            proxySupplier.connect_structured_push_consumer(
                    org.omg.CosNotifyComm.StructuredPushConsumerHelper.narrow(corbaRef));
        } catch (AcsJContainerServicesEx e) {
            LOG_NC_SubscriptionConnect_FAIL.log(logger, channelName, getNotificationFactoryName());
            throw new AcsJStateMachineActionEx(e);
        } catch (org.omg.CosEventChannelAdmin.AlreadyConnected e) {
            throw new AcsJStateMachineActionEx(new AcsJIllegalStateEventEx(e));
        } catch (org.omg.CosEventChannelAdmin.TypeError ex) {
            LOG_NC_SubscriptionConnect_FAIL.log(logger, channelName, getNotificationFactoryName());
            throw new AcsJStateMachineActionEx(ex);
        } catch (AcsJIllegalArgumentEx ex) {
            throw new AcsJStateMachineActionEx(ex);
        }

        LOG_NC_SubscriptionConnect_OK.log(logger, channelName, getNotificationFactoryName());
    }

    protected void destroyConnectionAction(EventDispatcher evtDispatcher, ErrorReporter errRep,
            SCInstance scInstance, Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        // TODO: CHeck if we need to suspend first (was like that in older impl)
        // try {
        // suspendAction(evtDispatcher, errRep, scInstance, derivedEvents);
        // } catch () {
        //
        // }

        /*
         * TODO: (rtobar) Maybe this code can be written more nicely, but always taking care that, if not in an illegal
         * state, then we should destroy the removed proxySupplier object
         */
        boolean success = false;

        try {
            // Clean up callback for reconnection requests
            channelReconnectionCallback.disconnect();

            // remove all filters and destroy the proxy supplier
            proxySupplier.remove_all_filters();

            try {
                // Clean up callback for subscribed events
                if (corbaRef != null) { // this check avoids ugly "offshoot was not activated" messages in certain scenarios
                    helper.getContainerServices().deactivateOffShoot(corbaObj);
                }
            } catch (AcsJContainerServicesEx e) {
                logger.log(Level.INFO, "Failed to Corba-deactivate NCSubscriber " + clientName, e);
            }

            logger.finer("Disconnected from NC '" + channelName + "'.");
            success = true;
        } catch (org.omg.CORBA.OBJECT_NOT_EXIST ex1) {
            // this is OK, because someone else has already destroyed the remote resources
            logger.fine("No need to release resources for channel " + channelName
                    + " because the NC has been destroyed already.");
            success = true;
            //      } catch (Throwable thr) {
            //         // TODO remove this hack
            //         throw new AcsJStateMachineActionEx(thr);
        } finally {
            if (success) {
                // null the refs if everything was fine, or if we got the OBJECT_NOT_EXIST
                channelReconnectionCallback = null;
                corbaRef = null;
                corbaObj = null;
            }
        }

        super.destroyConnectionAction(evtDispatcher, errRep, scInstance, derivedEvents);
    }

    protected void suspendAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance,
            Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        super.suspendAction(evtDispatcher, errRep, scInstance, derivedEvents);
        try {
            // See OMG NC spec 3.4.13.2. Server will continue to queue events.
            proxySupplier.suspend_connection();
        } catch (org.omg.CosNotifyChannelAdmin.ConnectionAlreadyInactive ex) {
            throw new AcsJStateMachineActionEx(ex);
        } catch (org.omg.CosNotifyChannelAdmin.NotConnected ex) {
            throw new AcsJStateMachineActionEx(ex);
        } catch (org.omg.CORBA.OBJECT_NOT_EXIST ex) {
            throw new AcsJStateMachineActionEx("Remote resources already destroyed.", ex);
        }
    }

    protected void resumeAction(EventDispatcher evtDispatcher, ErrorReporter errRep, SCInstance scInstance,
            Collection<TriggerEvent> derivedEvents) throws AcsJStateMachineActionEx {

        try {
            proxySupplier.resume_connection();
        } catch (org.omg.CosNotifyChannelAdmin.ConnectionAlreadyActive ex) {
            throw new AcsJStateMachineActionEx(ex);
        } catch (org.omg.CosNotifyChannelAdmin.NotConnected ex) {
            throw new AcsJStateMachineActionEx(ex);
        }
        super.resumeAction(evtDispatcher, errRep, scInstance, derivedEvents);
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////// Various template method impls //////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    @Override
    protected boolean isTraceEventsEnabled() {
        return isTraceNCEventsEnabled;
    }

    @Override
    protected void logEventReceiveHandlerException(String eventName, String receiverClassName, Throwable thr) {
        LOG_NC_EventReceive_HandlerException.log(logger, channelName, getNotificationFactoryName(), eventName,
                receiverClassName, thr.toString());
    }

    @Override
    protected void logEventProcessingTimeExceeded(String eventName, long logOcurrencesNumber) {
        LOG_NC_ProcessingTimeExceeded.log(logger, channelName, getNotificationFactoryName(), eventName,
                logOcurrencesNumber);
    }

    @Override
    protected void logEventProcessingTooSlowForEventRate(long numEventsDiscarded, String eventName) {
        LOG_NC_ReceiverTooSlow.log(logger, clientName, numEventsDiscarded, eventName, channelName,
                getNotificationFactoryName());
    }

    @Override
    protected void logNoEventReceiver(String eventName) {
        // With server-side filtering set up, we should never get an unexpected event type.
        // Thus we log this problem.
        LOG_NC_EventReceive_NoHandler.log(logger, channelName, getNotificationFactoryName(), eventName);
        if (noEventReceiverListener != null) {
            noEventReceiverListener.noEventReceiver(eventName);
        }
    }

    /**
     * To be used only for unit tests.
     */
    static interface NoEventReceiverListener {
        void noEventReceiver(String eventName);
    }

    /**
     * To be used only for unit tests.
     */
    void setNoEventReceiverListener(NoEventReceiverListener noEventReceiverListener) {
        this.noEventReceiverListener = noEventReceiverListener;
        logger.info("Will notify test listener '" + noEventReceiverListener.getClass().getName()
                + "' of events without matching receiver.");
    }

    @Override
    protected void logQueueShutdownError(int timeoutMillis, int remainingEvents) {
        logger.info(
                "Disconnecting from NC '" + channelName + "' before all events have been processed, in spite of "
                        + timeoutMillis + " 500 ms timeout grace period. " + remainingEvents
                        + " events are now still in the queue and may continue to be processed by the receiver.");
    }

    @Override
    protected double getMaxProcessTimeSeconds(String eventName) {
        if (!handlerTimeoutMap.containsKey(eventName)) {
            // setup a timeout if it's undefined
            handlerTimeoutMap.put(eventName, DEFAULT_MAX_PROCESS_TIME_SECONDS);
        }
        //System.out.println("Using handlerTimeout=" + handlerTimeoutMap.get(eventName) + " for event " + eventName);
        double maxProcessTimeSeconds = handlerTimeoutMap.get(eventName);
        return maxProcessTimeSeconds;
    }

    /**
     * Adds a filter on the server-side supplier proxy that lets the given event type pass through.
     * <p>
     * Note that we derive the event type name from the simple class name of <code>struct</code>, 
     * as done in other parts of ACS, which requires IDL event structs to have globally unique names 
     * across IDL name spaces.
     * <p>
     * If <code>structClass</code> is <code>null</code> (generic subscription), 
     * then "<code>*</code>" is used as the event type name,
     * which in ETCL is understood as a wildcard for all event type names. 
     * 
     * @param structClass
     * @throws AcsJEventSubscriptionEx
     */
    @Override
    protected void notifyFirstSubscription(Class<?> structClass) throws AcsJEventSubscriptionEx {
        String eventTypeNameShort = (structClass == null ? "*" : structClass.getSimpleName());
        try {
            int filterId = addFilter(eventTypeNameShort);
            subscriptionsFilters.put(eventTypeNameShort, filterId);
        } catch (AcsJCORBAProblemEx e) {
            throw new AcsJEventSubscriptionEx(e);
        }
    }

    @Override
    protected void notifySubscriptionRemoved(Class<?> structClass) throws AcsJEventSubscriptionEx {
        String eventTypeNameShort = (structClass == null ? "*" : structClass.getSimpleName());
        try {
            proxySupplier.remove_filter(subscriptionsFilters.get(eventTypeNameShort));
            subscriptionsFilters.remove(eventTypeNameShort);

            if (logger.isLoggable(AcsLogLevel.DELOUSE)) {
                NcFilterInspector insp = new NcFilterInspector(proxySupplier,
                        channelName + "::" + clientName + "::ProxySupplier", logger);
                logger.log(AcsLogLevel.DELOUSE,
                        "Removed filter for '" + eventTypeNameShort + "'. Current " + insp.getFilterInfo());
            }
        } catch (FilterNotFound e) {
            throw new AcsJEventSubscriptionEx(
                    "Filter for '" + eventTypeNameShort + "' not found on the server side: ", e);
        }
        // If receivers is empty we just discard everything
        if (receivers.isEmpty()) {
            discardAllEvents();
        }
    }

    @Override
    protected void notifyNoSubscription() {
        discardAllEvents();
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////////////// Helper methods  ////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Creates or reuses a shared server-side NC consumer admin object.
     * 
     * @throws AcsJException
     */
    private ConsumerAdmin getSharedAdmin() throws AcsJCORBAProblemEx, AcsJNarrowFailedEx {

        ConsumerAdmin ret = null;
        org.omg.CosNotifyChannelAdmin.ConsumerAdmin retBase = null;
        boolean created = false;
        int consumerAdminId = -1;
        AdminReuseCompatibilityHack adminReuseCompatibilityHack = new AdminReuseCompatibilityHack(channelName,
                logger);

        // @TODO (HSO): Why use a static lock here? This gives a false sense of safety in single-program unit tests,
        // while in real life we can have concurrent admin creation requests from different processes.
        synchronized (NCSubscriber.class) {

            // Check if we can reuse an already existing consumer admin
            for (int adminId : channel.get_all_consumeradmins()) {
                try {
                    org.omg.CosNotifyChannelAdmin.ConsumerAdmin tmpAdmin = channel.get_consumeradmin(adminId);
                    if (adminReuseCompatibilityHack.isSharedAdmin(tmpAdmin)) {
                        // do some simple load balancing, so we use this shared admin only if it has space for more proxies
                        // (the -1 goes because of the dummy proxy that is attached to the shared admin)
                        if (tmpAdmin.push_suppliers().length - 1 < PROXIES_PER_ADMIN) {
                            retBase = tmpAdmin;
                            consumerAdminId = adminId;
                            break;
                        }
                    }
                } catch (AdminNotFound e) {
                    logger.log(AcsLogLevel.NOTICE,
                            "Consumer admin with ID='" + adminId + "' not found for channel '" + channelName + "', "
                                    + "will continue anyway to search for shared consumer admins",
                            e);
                }
            }

            // If no suitable consumer admin was found, we create a new one 
            if (retBase == null) {

                // create a new consumer admin
                IntHolder consumerAdminIDHolder = new IntHolder();
                // We use filters only on proxy objects, not on admin objects.
                // An admin object without filters will opt to pass all events.
                // We need a logical AND to be used when comparing the event passing decisions
                // made by the set of proxy supplier filters and by the admin object.
                InterFilterGroupOperator adminProxyFilterLogic = InterFilterGroupOperator.AND_OP;
                retBase = channel.new_for_consumers(adminProxyFilterLogic, consumerAdminIDHolder);

                consumerAdminId = consumerAdminIDHolder.value;
                created = true;
            }

        } // synchronize(NCSubscriber.class) ...

        try {
            // cast to TAO extension type
            ret = ConsumerAdminHelper.narrow(retBase);
        } catch (BAD_PARAM ex) {

            if (created) {
                retBase.destroy();
            }
            LOG_NC_TaoExtensionsSubtypeMissing.log(logger, "ConsumerAdmin for channel " + channelName,
                    ConsumerAdminHelper.id(), org.omg.CosNotifyChannelAdmin.ConsumerAdminHelper.id());
            AcsJNarrowFailedEx ex2 = new AcsJNarrowFailedEx(ex);
            ex2.setNarrowType(ConsumerAdminHelper.id());
            throw ex2;
        }

        if (created) {
            // @TODO: Remove this workaround once it is no longer needed.
            adminReuseCompatibilityHack.markAsSharedAdmin(ret);
        }

        LOG_NC_ConsumerAdminObtained_OK.log(logger, consumerAdminId, (created ? "created" : "reused"), clientName,
                channelName, getNotificationFactoryName());

        return ret;
    }

    /**
     * Creates the proxy supplier (push-style, for structured events) 
     * that lives in the Notify server process, managed by the consumer admin object, and
     * will later be connected to this client-side subscriber object.
     * 
     * @throws AcsJCORBAProblemEx If creation of the proxy supplier failed.
     */
    private StructuredProxyPushSupplier createProxySupplier() throws AcsJCORBAProblemEx {

        StructuredProxyPushSupplier ret = null;
        String errMsg = null;
        IntHolder proxyIdHolder = new IntHolder(); // will get assigned "a numeric identifier [...] that is unique among all proxy suppliers [the admin object] has created"

        String randomizedClientName = null;
        try {
            ProxySupplier proxy = null;
            while (proxy == null) {
                // See the comments on Consumer#createConsumer() for a nice explanation of why this randomness is happening here
                randomizedClientName = Helper.createRandomizedClientName(clientName);
                try {
                    proxy = sharedConsumerAdmin.obtain_named_notification_push_supplier(ClientType.STRUCTURED_EVENT,
                            proxyIdHolder, randomizedClientName);
                } catch (NameAlreadyUsed e) {
                    // Hopefully we won't run into this situation. Still, try to go on in the loop,
                    // with a different client name next time.
                } catch (NameMapError e) {
                    // Default to the unnamed version
                    proxy = sharedConsumerAdmin.obtain_notification_push_supplier(ClientType.STRUCTURED_EVENT,
                            proxyIdHolder);
                }
            }
            ret = StructuredProxyPushSupplierHelper.narrow(proxy);
        } catch (AdminLimitExceeded ex) {
            // See NC spec 3.4.15.10
            // If the number of consumers currently connected to the channel with which the target ConsumerAdmin object is associated 
            // exceeds the value of the MaxConsumers administrative property, the AdminLimitExceeded exception is raised.
            String limit = ex.admin_property_err.value.extract_string();
            errMsg = "NC '" + channelName + "' is configured for a maximum of " + limit
                    + " subscribers, which does not allow this client to subscribe.";
        }

        if (ret != null) {
            LOG_NC_SupplierProxyCreation_OK.log(logger, proxyIdHolder.value, clientName, randomizedClientName,
                    channelName, getNotificationFactoryName());
        } else {
            LOG_NC_SupplierProxyCreation_FAIL.log(logger, clientName, channelName, getNotificationFactoryName(),
                    errMsg);
            AcsJCORBAProblemEx ex2 = new AcsJCORBAProblemEx();
            ex2.setInfo("Failed to create proxy supplier on NC '" + channelName + "' for client '" + clientName
                    + "': " + errMsg);
            throw ex2;
        }
        return ret;
    }

    /**
     * This method manages the filtering capabilities used to control subscriptions.
     * <p>
     * A constraint evaluates to true when both of the following conditions are true:
     *   A member of the constraint's EventTypeSeq matches the message's event type.
     *   The constraint expression evaluates to true.
     * 
     * @return FilterID (see OMG NotificationService spec 3.2.4.1)
     * @throws AcsJCORBAProblemEx
     */
    protected int addFilter(String eventTypeName) throws AcsJCORBAProblemEx {

        try {
            // Create the filter
            FilterFactory filterFactory = channel.default_filter_factory();
            Filter filter = filterFactory.create_filter(getFilterLanguage());

            // Information needed to construct the constraint expression object
            // (any domain, THE event type)
            // Note that TAO will internally convert the event type name 
            // to the expression "$type_name=='<our_eventTypeName>'", 
            // see orbsvcs/Notify/Notify_Constraint_Interpreter.cpp
            EventType[] t_info = { new EventType("*", eventTypeName) }; // The old Consumer class used 'getChannelDomain()' instead of "*"..?

            // Add constraint expression object to the filter
            String constraint_expr = ""; // no constraints other than the eventTypeName already given above
            ConstraintExp[] cexp = { new ConstraintExp(t_info, constraint_expr) };
            filter.add_constraints(cexp);

            // Add the filter to the proxy and return the filter ID
            int filterId = proxySupplier.add_filter(filter);

            if (logger.isLoggable(AcsLogLevel.DELOUSE)) {
                NcFilterInspector insp = new NcFilterInspector(proxySupplier,
                        channelName + "::" + clientName + "::ProxySupplier", logger);
                logger.log(AcsLogLevel.DELOUSE,
                        "Added filter for '" + eventTypeName + "'. Current " + insp.getFilterInfo());

                //            NcFilterInspector insp2 = new NcFilterInspector(
                //                  sharedConsumerAdmin, channelName + "::" + clientName + "::Admin", logger);
                //            logger.log(AcsLogLevel.DEBUG, "Admin filters: " + insp2.getFilterInfo());
            }
            return filterId;

        } catch (org.omg.CosNotifyFilter.InvalidGrammar e) {
            Throwable cause = new Throwable("'" + eventTypeName + "' filter is invalid for the '" + channelName
                    + "' channel: " + e.getMessage());
            throw new alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx(cause);
        } catch (org.omg.CosNotifyFilter.InvalidConstraint e) {
            Throwable cause = new Throwable("'" + eventTypeName + "' filter is invalid for the '" + channelName
                    + "' channel: " + e.getMessage());
            throw new alma.ACSErrTypeCommon.wrappers.AcsJCORBAProblemEx(cause);
        }

    }

    /**
     * This method is used to discard all events. It is called when there are no
     * subscriptions left or if the {@link #removeSubscription()} method is
     * called with null as parameter.
     */
    private void discardAllEvents() {

        // For safety, remove all filters in the server, clear the local references,
        // and put a dummy filter that filters out everything
        proxySupplier.remove_all_filters();
        subscriptionsFilters.clear();
        try {
            // If no filters are attached, the default behavior is to pass all events.
            // Thus we attach a dummy forwarding filter, to enable the one-filter-must-match behavior.
            addFilter("EVENT_TYPE_THAT_NEVER_MATCHES");
            // TODO: It seems that once a filter was added, calling 'remove_all_filters' does not restore
            //       the "pass all" behavior, cf. NCSubscriberTest#testServerSideEventTypeFiltering() comments.
            //       Thus we may be able to delete this dummy filter again, although it seems a bit risky.
        } catch (AcsJCORBAProblemEx e) {
            logger.log(AcsLogLevel.ERROR,
                    "Cannot add all-exclusive filter, we'll keep receiving events, but no handler will receive them");
        }
    }

    /**
     * This method returns the notify service name as registered with the CORBA
     * Naming Service. This is normally equivalent to
     * <code>NotifyEventChannelFactory</code>.
     * 
     * @return string
     */
    protected String getNotificationFactoryName() {
        return helper.getNotificationFactoryNameForChannel();
    }

    /**
     * 
     * This method returns a string to the type of filter constraint language to
     * be used for filtering events, which is normally equivalent to
     * acsnc::FILTER_LANGUAGE_NAME (ETCL = Extended Trader Constraint Language).
     * 
     * @return pointer to a constant string.
     */
    protected String getFilterLanguage() {
        return alma.acsnc.FILTER_LANGUAGE_NAME.value;
    }

    ////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////////// Corba callback methods  ////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    /**
     * This method is called by the notification channel (supplier proxy) each time an event is received.
     * <p>
     * It is declared <code>final</code> because it is crucial for the functioning of the NC library
     * and thus cannot be overwritten by a subclass. 
     * If for special purposes a notification of raw event reception is needed, 
     * a subclass can implement {@link #push_structured_event_called(StructuredEvent)}, which gets called from this
     * method as the first thing it does. 
     * @param structuredEvent
     *            The structured event sent by a supplier.
     * @throws Disconnected If this subscriber is disconnected from the NC. 
     *         See NC spec 3.3.7.1: "if the invocation of push_structured_event upon a StructuredPushConsumer instance 
     *         by a StructuredProxyPushSupplier instance results in the Disconnected exception being raised, 
     *         the StructuredProxyPushSupplier will invoke its own disconnect_structured_push_supplier operation, 
     *         resulting in the destruction of that StructuredProxyPushSupplier instance."
     *         This serves only as a backup mechanism, since normally we explicitly disconnect the subscriber.
     * 
     * @see org.omg.CosNotifyComm.StructuredPushConsumerOperations#push_structured_event(org.omg.CosNotification.StructuredEvent)
     */
    @Override
    public final void push_structured_event(StructuredEvent structuredEvent) throws Disconnected {

        boolean shouldProcessEvent = true;

        try {
            shouldProcessEvent = push_structured_event_called(structuredEvent);
        } catch (Throwable thr) {
            // ignore any exception, since push_structured_event_called is only meant for 
            // notification, to enable special tests or other exotic purposes.
            // In this case we also keep shouldProcessEvent=true, just in case.
            // TODO: It may be better to treat the exception like shouldProcessEvent==false
            //       since non-struct event data will cause more errors further down.
        }

        // got a subclass 'veto'?
        if (!shouldProcessEvent) {
            if (firstSubclassVeto) {
                logger.info("Event subscriber '" + getClass().getSimpleName()
                        + "' handles one or more raw NC events itself, bypassing base class '"
                        + NCSubscriber.class.getName()
                        + "'. This non-standard behavior will not be logged again by this NCSubscriber.");
                firstSubclassVeto = false;
            }
            return;
        }

        if (isDisconnected()) {
            throw new Disconnected();
        }

        Object convertedAny = anyAide.complexAnyToObject(structuredEvent.filterable_data[0].value);

        if (convertedAny == null) {
            // @TODO: compare with ACS-NC specs and C++ impl, and perhaps call generic receiver with null data,
            //        if the event does not carry any data.
            LOG_NC_EventReceive_FAIL.log(logger, channelName, getNotificationFactoryName(),
                    structuredEvent.header.fixed_header.event_type.type_name, "null");
        } else {
            // An optimization: If the event type cannot match a typed or generic receiver
            // then we don't put it into the queue. We could improve this by checking for registered receivers already here...
            if (!eventType.isInstance(convertedAny) && !hasGenericReceiver()) {
                logNoEventReceiver(convertedAny.getClass().getName());
            }

            EventDescription eventDesc = EventDescriptionHelper.extract(structuredEvent.remainder_of_body);

            if (isTraceEventsEnabled()) {
                LOG_NC_EventReceive_OK.log(logger, channelName, getNotificationFactoryName(),
                        structuredEvent.header.fixed_header.event_type.type_name);
            }

            // let the base class deal with queue and dispatching to receiver
            processEventAsync(convertedAny, eventDesc);
        }
    }

    /**
     * Users can override this method to get notified of raw events, for additional statistics, 
     * to handle event data given as a sequence of IDL structs (exceptional case in acssamp),
     * or for DynAny access (eventGUI).
     * <p>
     * Usually this method should not be overridden.
     * 
     * @param structuredEvent
     * @return <code>true</code> if normal event processing should continue, 
     *         <code>false</code> if NCSubscriber should not process this event. 
     */
    protected boolean push_structured_event_called(StructuredEvent structuredEvent) {
        //System.out.println("********** got a call to push_structured_event **********");
        return true;
    }

    /**
     * ACS does not provide an implementation of this method.
     * 
     * @see org.omg.CosNotifyComm.StructuredPushConsumerOperations#disconnect_structured_push_consumer()
     * @throws NO_IMPLEMENT
     */
    @Override
    public void disconnect_structured_push_consumer() {
        throw new NO_IMPLEMENT();

    }

    /**
     * ACS does not provide an implementation of this method.
     * 
     * @see org.omg.CosNotifyComm.NotifyPublishOperations#offer_change(org.omg.CosNotification.EventType[],
     *      org.omg.CosNotification.EventType[])
     * @throws NO_IMPLEMENT
     */
    @Override
    public void offer_change(EventType[] added, EventType[] removed) throws InvalidEventType {
        throw new NO_IMPLEMENT();
    }

    /**
     * @TODO: Perhaps integrate reconnection into the state machine.
     * 
     * @see alma.acs.nc.ReconnectableParticipant#reconnect(gov.sandia.NotifyMonitoringExt.EventChannelFactory)
     */
    @Override
    public void reconnect(EventChannelFactory ecf) {

        logger.log(AcsLogLevel.NOTICE,
                "Reconnecting subscriber with channel '" + channelName + "' after Notify Service recovery");

        if (channel != null) {
            channel = helper.getNotificationChannel(ecf);
            if (channel == null) {
                logger.log(Level.WARNING, "Cannot reconnect to the channel '" + channelName + "'");
                return;
            }
        }

        try {
            channel.set_qos(helper.getChannelProperties().getCDBQoSProps(channelName));
            channel.set_admin(helper.getChannelProperties().getCDBAdminProps(channelName));
        } catch (UnsupportedQoS e) {
        } catch (AcsJException e) {
        } catch (UnsupportedAdmin ex) {
            logger.warning(helper.createUnsupportedAdminLogMessage(ex));
        }

    }

    ////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////////// AdminReuseCompatibilityHack  ////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    /**
     * Encapsulates the hack of using a dummy ProxyType.PUSH_ANY proxy to mark a shared consumer admin used by NCSubscriber
     * (or other next-gen subscribers), to distinguish it from non-reusable (subscriber-owned) admin objects 
     * as they are used by the old-generation subscribers.
     * <p>
     * @TODO: Remove this class once the hack is no longer needed.
     */
    public static class AdminReuseCompatibilityHack {

        public static final String DUMMY_SUPPLIER_PROXY_NAME_PREFIX = "dummyproxy";

        private final String channelName;
        private final Logger logger;

        public AdminReuseCompatibilityHack(String channelName, Logger logger) {
            this.channelName = channelName;
            this.logger = logger;
        }

        /**
         * Creates a dummy proxy in the given consumer admin.
         * This dummy proxy is not of a StructuredProxyPushSupplier, like the rest of the proxies that are created for the NC in the admin objects. 
         * This way we can recognize a shared consumer admin by looking at its proxies, and checking if their "MyType" property is ANY_EVENT.
         * This hack is only needed while we are in transition between the old and new NC classes, and should get removed once the old classes are not used 
         * anymore (also not in C++ etc).
         * In addition to using a unique proxy type, we also use a recognizable name, 
         * so that we can recognize the dummy proxy even when we only know its name,
         * as it happens when working with the TAO MC statistics, see {@link #isDummyProxy(String)}.
         * 
         * @throws AcsJCORBAProblemEx 
         */
        public void markAsSharedAdmin(ConsumerAdmin consumerAdmin) throws AcsJCORBAProblemEx {
            try {
                // There should be only one dummy proxy per shared admin, but any two concurrent subscribers
                // should rather create a dummy proxy too many than getting an exception. Thus we make the name unique.
                String dummySupplierName = Helper.createRandomizedClientName(DUMMY_SUPPLIER_PROXY_NAME_PREFIX);

                consumerAdmin.obtain_named_notification_push_supplier(ClientType.ANY_EVENT, new IntHolder(),
                        dummySupplierName);
            } catch (Exception ex) {
                // This ex could be AdminLimitExceeded, NameAlreadyUsed, NameMapError
                consumerAdmin.destroy();
                AcsJCORBAProblemEx e2 = new AcsJCORBAProblemEx(ex);
                e2.setInfo(
                        "Coundn't attach dummy ANY_EVENT proxy to newly created shared admin consumer for channel '"
                                + channelName + "'. Newly created shared admin is now destroyed.");
                throw e2;
            }
        }

        /**
         * Checks if a given proxy supplier (as obtained through the regular NC API)
         * is a dummy produced by this class.
         * 
         * @return <code>true</code> if the given proxy is of type PUSH_ANY,
         *         which is used only to mark a shared consumer admin. 
         */
        public static boolean isDummyProxy(ProxySupplier proxy) {
            return (ProxyType.PUSH_ANY.equals(proxy.MyType()));
        }

        /**
         * Checks if a given proxy supplier (as obtained by name through the TAO MC extension API) is a dummy produced
         * by this class.
         * 
         * @return <code>true</code> if the given proxy name starts with {@link #DUMMY_SUPPLIER_PROXY_NAME_PREFIX},
         *         which is used only to mark a shared consumer admin.
         */
        public static boolean isDummyProxy(String proxyName) {
            return (proxyName.startsWith(DUMMY_SUPPLIER_PROXY_NAME_PREFIX));
        }

        /**
         * @return <code>true</code> if our consumer admin is shared. In the future when all NC libs are ported, this should always be the case.
         */
        public boolean isSharedAdmin(org.omg.CosNotifyChannelAdmin.ConsumerAdmin consumerAdmin) {
            boolean ret = false;
            int[] push_suppliers_ids = consumerAdmin.push_suppliers();
            for (int proxyID : push_suppliers_ids) {
                try {
                    ProxySupplier proxy = consumerAdmin.get_proxy_supplier(proxyID);
                    if (isDummyProxy(proxy)) {
                        ret = true;
                        break;
                    }
                } catch (ProxyNotFound e) {
                    logger.log(AcsLogLevel.NOTICE,
                            "Proxy with ID='" + proxyID + "' not found for consumer admin with ID='"
                                    + consumerAdmin.MyID() + "', "
                                    + "will continue anyway to search for shared consumer admins",
                            e);
                }
            }
            return ret;
        }

    }

}