/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.cache.pojo.jmx;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheException;
import org.jboss.cache.CacheStatus;
import org.jboss.cache.config.BuddyReplicationConfig;
import org.jboss.cache.config.CacheLoaderConfig;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.ConfigurationException;
import org.jboss.cache.config.EvictionConfig;
import org.jboss.cache.config.RuntimeConfig;
import org.jboss.cache.factories.XmlConfigurationParser;
import org.jboss.cache.jmx.CacheJmxWrapper;
import org.jboss.cache.jmx.CacheNotificationListener;
import org.jboss.cache.pojo.PojoCache;
import org.jboss.cache.pojo.PojoCacheAlreadyDetachedException;
import org.jboss.cache.pojo.PojoCacheException;
import org.jboss.cache.pojo.PojoCacheFactory;
import org.jboss.cache.pojo.impl.PojoCacheImpl;
import org.jgroups.Channel;
import org.jgroups.ChannelFactory;
import org.jgroups.jmx.JChannelFactoryMBean;
import org.w3c.dom.Element;

import javax.management.AttributeChangeNotification;
import javax.management.JMException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.transaction.TransactionManager;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class PojoCacheJmxWrapper
        implements PojoCacheJmxWrapperMBean, MBeanRegistration, NotificationEmitter
{
   private Log log = LogFactory.getLog(getClass().getName());

   private boolean registerInterceptors = true;
   private Configuration config;
   private MBeanServer server;
   private String cacheObjectName;
   private PojoCache pojoCache;
   private CacheJmxWrapper plainCacheWrapper;
   private boolean registerPlainCache = true;
   private boolean plainCacheRegistered;
   private CacheStatus cacheStatus;
   private boolean registered;
   private final Set<NotificationListenerArgs> pendingListeners =
           new HashSet<NotificationListenerArgs>();

   // Legacy config support

   private Element buddyReplConfig;
   private Element evictionConfig;
   private Element cacheLoaderConfig;
   private Element clusterConfig;
   private JChannelFactoryMBean multiplexerService;

   /**
    * Default constructor.
    */
   public PojoCacheJmxWrapper()
   {
      cacheStatus = CacheStatus.INSTANTIATED;
   }

   /**
    * Creates a PojoCacheJmxWrapper that wraps the given PojoCache.
    *
    * @param toWrap the cache
    */
   public PojoCacheJmxWrapper(PojoCache toWrap)
   {
      this();
      setPojoCache(toWrap);
   }

   // PojoCacheMBean

   public PojoCache getPojoCache()
   {
      return pojoCache;
   }

   public Configuration getConfiguration()
   {
      Configuration cfg = (pojoCache == null ? config : pojoCache.getCache().getConfiguration());
      if (cfg == null)
      {
         cfg = config = new Configuration();
      }
      return cfg;
   }

   public String getInternalLocation(Object pojo) throws PojoCacheAlreadyDetachedException
   {
      return pojoCache.getPojoID(pojo);
   }

   public String getUnderlyingCacheObjectName()
   {
      return plainCacheWrapper == null ? null : plainCacheWrapper.getCacheObjectName();
   }

   public void create() throws PojoCacheException
   {
      if (cacheStatus.createAllowed() == false)
      {
         if (cacheStatus.needToDestroyFailedCache())
            destroy();
         else
            return;
      }

      try
      {
         cacheStatus = CacheStatus.CREATING;

         if (pojoCache == null)
         {
            if (config == null)
            {
               throw new ConfigurationException("Must call setConfiguration() " +
                       "or setPojoCache() before call to create()");
            }

            constructCache();
         }

         pojoCache.create();

         registerPlainCache();

         plainCacheWrapper.create();

         cacheStatus = CacheStatus.CREATED;
      }
      catch (Throwable t)
      {
         handleLifecycleTransitionFailure(t);
      }
   }

   public void start() throws PojoCacheException
   {
      if (cacheStatus.startAllowed() == false)
      {
         if (cacheStatus.needToDestroyFailedCache())
            destroy();
         if (cacheStatus.needCreateBeforeStart())
            create();
         else
            return;
      }

      try
      {
         int oldState = getState();
         cacheStatus = CacheStatus.STARTING;
         int startingState = getState();
         sendStateChangeNotification(oldState, startingState, getClass().getSimpleName() + " starting", null);

         pojoCache.start();

         plainCacheWrapper.start();
         cacheStatus = CacheStatus.STARTED;
         sendStateChangeNotification(startingState, getState(), getClass().getSimpleName() + " started", null);
      }
      catch (Throwable t)
      {
         handleLifecycleTransitionFailure(t);
      }
   }

   public void stop()
   {
      if (cacheStatus.stopAllowed() == false)
      {
         return;
      }

      // Trying to stop() from FAILED is valid, but may not work
      boolean failed = cacheStatus == CacheStatus.FAILED;

      try
      {
         int oldState = getState();
         cacheStatus = CacheStatus.STOPPING;
         int stoppingState = getState();
         sendStateChangeNotification(oldState, stoppingState, getClass().getSimpleName() + " stopping", null);

         cacheStatus = CacheStatus.STOPPING;

         pojoCache.stop();

         plainCacheWrapper.stop();
         cacheStatus = CacheStatus.STOPPED;
         sendStateChangeNotification(stoppingState, getState(), getClass().getSimpleName() + " stopped", null);
      }
      catch (Throwable t)
      {
         if (failed)
         {
            log.warn("Attempted to stop() from FAILED state, " +
                    "but caught exception; try calling destroy()", t);
         }
         handleLifecycleTransitionFailure(t);
      }
   }

   public void destroy()
   {
      if (cacheStatus.destroyAllowed() == false)
      {
         if (cacheStatus.needStopBeforeDestroy())
            stop();
         else
            return;
      }

      try
      {
         cacheStatus = CacheStatus.DESTROYING;

         pojoCache.destroy();

         // The cache is destroyed, so we shouldn't leave it registered
         // in JMX even if we didn't register it in create()
         unregisterPlainCache();

         plainCacheWrapper.destroy();
      }
      finally
      {
         // We always proceed to DESTROYED
         cacheStatus = CacheStatus.DESTROYED;
      }
   }

   public CacheStatus getCacheStatus()
   {
      return cacheStatus;
   }

   public int getState()
   {
      switch (cacheStatus)
      {
         case INSTANTIATED:
         case CREATING:
            return registered ? REGISTERED : UNREGISTERED;
         case CREATED:
            return CREATED;
         case STARTING:
            return STARTING;
         case STARTED:
            return STARTED;
         case STOPPING:
            return STOPPING;
         case STOPPED:
         case DESTROYING:
            return STOPPED;
         case DESTROYED:
            return registered ? DESTROYED : UNREGISTERED;
         case FAILED:
         default:
            return FAILED;
      }
   }

   public boolean getRegisterPlainCache()
   {
      return registerPlainCache;
   }

   public void setRegisterPlainCache(boolean registerPlainCache)
   {
      this.registerPlainCache = registerPlainCache;
   }

   public boolean getRegisterInterceptors()
   {
      return registerInterceptors;
   }

   public void setRegisterInterceptors(boolean register)
   {
      this.registerInterceptors = register;
   }

   // ----------------------------------------------------  LegacyConfiguration

   public Element getBuddyReplicationConfig()
   {
      return buddyReplConfig;
   }

   public Element getCacheLoaderConfig()
   {
      return cacheLoaderConfig;
   }

   public Element getCacheLoaderConfiguration()
   {
      return getCacheLoaderConfig();
   }

   public String getCacheMode()
   {
      return getConfiguration().getCacheModeString();
   }

   public String getClusterName()
   {
      return getConfiguration().getClusterName();
   }

   public String getClusterProperties()
   {
      return getConfiguration().getClusterConfig();
   }

   public Element getClusterConfig()
   {
      return clusterConfig;
   }

   public Element getEvictionPolicyConfig()
   {
      return evictionConfig;
   }

   public boolean getExposeManagementStatistics()
   {
      return getConfiguration().getExposeManagementStatistics();
   }

   public boolean getUseInterceptorMbeans()
   {
      return getExposeManagementStatistics();
   }

   public boolean getFetchInMemoryState()
   {
      return getConfiguration().isFetchInMemoryState();
   }

   public long getStateRetrievalTimeout()
   {
      return getConfiguration().getStateRetrievalTimeout();
   }

   @Deprecated
   public void setInitialStateRetrievalTimeout(long timeout)
   {
      setStateRetrievalTimeout(timeout);
   }

   public String getIsolationLevel()
   {
      return getConfiguration().getIsolationLevelString();
   }

   public long getLockAcquisitionTimeout()
   {
      return getConfiguration().getLockAcquisitionTimeout();
   }

   public JChannelFactoryMBean getMultiplexerService()
   {
      return multiplexerService;
   }

   public String getMultiplexerStack()
   {
      return getConfiguration().getMultiplexerStack();
   }

   public ChannelFactory getMuxChannelFactory()
   {
      return getConfiguration().getRuntimeConfig().getMuxChannelFactory();
   }

   public String getNodeLockingScheme()
   {
      return getConfiguration().getNodeLockingSchemeString();
   }

   public long getReplQueueInterval()
   {
      return getConfiguration().getReplQueueInterval();
   }

   public int getReplQueueMaxElements()
   {
      return getConfiguration().getReplQueueMaxElements();
   }

   public String getReplicationVersion()
   {
      return getConfiguration().getReplVersionString();
   }

   public boolean getSyncCommitPhase()
   {
      return getConfiguration().isSyncCommitPhase();
   }

   public long getSyncReplTimeout()
   {
      return getConfiguration().getSyncReplTimeout();
   }

   public boolean getSyncRollbackPhase()
   {
      return getConfiguration().isSyncRollbackPhase();
   }

   public TransactionManager getTransactionManager()
   {
      return getConfiguration().getRuntimeConfig().getTransactionManager();
   }

   public String getTransactionManagerLookupClass()
   {
      return getConfiguration().getTransactionManagerLookupClass();
   }

   public boolean getUseRegionBasedMarshalling()
   {
      return getConfiguration().isUseRegionBasedMarshalling();
   }

   public boolean getUseReplQueue()
   {
      return getConfiguration().isUseReplQueue();
   }

   public boolean isInactiveOnStartup()
   {
      return getConfiguration().isInactiveOnStartup();
   }

   public void setBuddyReplicationConfig(Element config)
   {
      BuddyReplicationConfig brc = null;
      if (config != null)
      {
         brc = XmlConfigurationParser.parseBuddyReplicationConfig(config);
      }
      getConfiguration().setBuddyReplicationConfig(brc);
      this.buddyReplConfig = config;
   }

   public void setCacheLoaderConfig(Element cache_loader_config)
   {
      CacheLoaderConfig clc = null;
      if (cache_loader_config != null)
      {
         clc = XmlConfigurationParser.parseCacheLoaderConfig(cache_loader_config);
      }
      getConfiguration().setCacheLoaderConfig(clc);
      this.cacheLoaderConfig = cache_loader_config;
   }

   public void setCacheLoaderConfiguration(Element config)
   {
      log.warn("MBean attribute 'CacheLoaderConfiguration' is deprecated; " +
              "use 'CacheLoaderConfig'");
      setCacheLoaderConfig(config);
   }

   public void setCacheMode(String mode) throws Exception
   {
      getConfiguration().setCacheModeString(mode);
   }

   public void setClusterConfig(Element config)
   {
      String props = null;
      if (config != null)
      {
         props = XmlConfigurationParser.parseClusterConfigXml(config);
      }
      getConfiguration().setClusterConfig(props);
      this.clusterConfig = config;
   }

   @Deprecated
   public long getInitialStateRetrievalTimeout()
   {
      return getStateRetrievalTimeout();
   }

   public void setClusterName(String name)
   {
      getConfiguration().setClusterName(name);
   }

   public void setClusterProperties(String cluster_props)
   {
      getConfiguration().setClusterConfig(cluster_props);
   }

   public void setEvictionPolicyConfig(Element config)
   {
      EvictionConfig ec = null;
      if (config != null)
      {
         ec = XmlConfigurationParser.parseEvictionConfig(config);
      }
      getConfiguration().setEvictionConfig(ec);
      this.evictionConfig = config;
   }

   public void setExposeManagementStatistics(boolean expose)
   {
      getConfiguration().setExposeManagementStatistics(expose);
   }

   public void setUseInterceptorMbeans(boolean use)
   {
      log.warn("MBean attribute 'UseInterceptorMbeans' is deprecated; " +
              "use 'ExposeManagementStatistics'");
      setExposeManagementStatistics(use);
   }

   public void setFetchInMemoryState(boolean flag)
   {
      getConfiguration().setFetchInMemoryState(flag);
   }

   public void setInactiveOnStartup(boolean inactiveOnStartup)
   {
      getConfiguration().setInactiveOnStartup(inactiveOnStartup);
   }

   public void setStateRetrievalTimeout(long timeout)
   {
      getConfiguration().setStateRetrievalTimeout(timeout);
   }

   public void setIsolationLevel(String level)
   {
      getConfiguration().setIsolationLevelString(level);
   }

   public void setLockAcquisitionTimeout(long timeout)
   {
      getConfiguration().setLockAcquisitionTimeout(timeout);
   }

   public void setMultiplexerService(JChannelFactoryMBean muxService)
   {
      this.multiplexerService = muxService;
   }

   public void setMultiplexerStack(String stackName)
   {
      getConfiguration().setMultiplexerStack(stackName);
   }

   public void setMuxChannelFactory(ChannelFactory factory)
   {
      getConfiguration().getRuntimeConfig().setMuxChannelFactory(factory);
   }

   public void setNodeLockingScheme(String nodeLockingScheme)
   {
      getConfiguration().setNodeLockingSchemeString(nodeLockingScheme);
   }

   public void setReplQueueInterval(long interval)
   {
      getConfiguration().setReplQueueInterval(interval);
   }

   public void setReplQueueMaxElements(int max_elements)
   {
      getConfiguration().setReplQueueMaxElements(max_elements);
   }

   public void setReplicationVersion(String version)
   {
      getConfiguration().setReplVersionString(version);
   }

   public void setSyncCommitPhase(boolean sync_commit_phase)
   {
      getConfiguration().setSyncCommitPhase(sync_commit_phase);
   }

   public void setSyncReplTimeout(long timeout)
   {
      getConfiguration().setSyncReplTimeout(timeout);
   }

   public void setSyncRollbackPhase(boolean sync_rollback_phase)
   {
      getConfiguration().setSyncRollbackPhase(sync_rollback_phase);
   }

   public void setTransactionManager(TransactionManager manager)
   {
      getConfiguration().getRuntimeConfig().setTransactionManager(manager);
   }

   public void setTransactionManagerLookupClass(String cl) throws Exception
   {
      getConfiguration().setTransactionManagerLookupClass(cl);
   }

   public void setUseRegionBasedMarshalling(boolean isTrue)
   {
      getConfiguration().setUseRegionBasedMarshalling(isTrue);
   }

   public void setUseReplQueue(boolean flag)
   {
      getConfiguration().setUseReplQueue(flag);
   }

   // ------------------------------------------------------  MBeanRegistration

   /**
    * Caches the provided <code>server</code> and <code>objName</code>.
    */
   public ObjectName preRegister(MBeanServer server, ObjectName objName)
           throws Exception
   {
      this.server = server;

      if (cacheObjectName == null)
      {
         cacheObjectName = objName.getCanonicalName();
      }

      if (plainCacheWrapper != null)
         plainCacheWrapper.setNotificationServiceName(cacheObjectName);

      return new ObjectName(cacheObjectName);
   }

   /**
    * Registers the CacheJmxWrapperMBean,
    * if {@link #getRegisterPlainCache()} is <code>true</code>.
    */
   public void postRegister(Boolean registrationDone)
   {
      if (Boolean.TRUE.equals(registrationDone) && registerPlainCache)
      {
         log.debug("Registered in JMX under " + cacheObjectName);

         if (plainCacheWrapper != null)
         {
            try
            {
               registerPlainCache();
            }
            catch (Exception e)
            {
               log.error("Caught exception registering plain cache with JMX", e);
            }
         }

         registered = true;
      }
   }

   /**
    * No-op.
    */
   public void preDeregister() throws Exception
   {
   }

   /**
    * Unregisters the CacheJmxWrapper, if {@link #getRegisterPlainCache()} is
    * <code>true</code>.
    */
   public void postDeregister()
   {
      if (plainCacheWrapper != null)
      {
         unregisterPlainCache();
      }

      registered = false;
   }

   // ----------------------------------------------------  NotificationEmitter 

   public void removeNotificationListener(NotificationListener listener,
                                          NotificationFilter filter,
                                          Object handback)
           throws ListenerNotFoundException
   {
      synchronized (pendingListeners)
      {
         boolean found = pendingListeners.remove(new NotificationListenerArgs(listener, filter, handback));

         if (plainCacheWrapper != null)
         {
            plainCacheWrapper.removeNotificationListener(listener, filter, handback);
         }
         else if (!found)
         {
            throw new ListenerNotFoundException();
         }
      }
   }

   public void addNotificationListener(NotificationListener listener,
                                       NotificationFilter filter,
                                       Object handback)
           throws IllegalArgumentException
   {
      synchronized (pendingListeners)
      {
         if (plainCacheWrapper != null)
         {
            plainCacheWrapper.addNotificationListener(listener, filter, handback);
         }
         else
         {
            // Add it for addition to the plainCacheWrapper when it's created
            pendingListeners.add(new NotificationListenerArgs(listener, filter, handback));
         }
      }

   }

   public MBeanNotificationInfo[] getNotificationInfo()
   {
      return CacheNotificationListener.getNotificationInfo();
   }

   public void removeNotificationListener(NotificationListener listener)
           throws ListenerNotFoundException
   {
      synchronized (pendingListeners)
      {
         boolean found = false;
         for (Iterator<NotificationListenerArgs> iter = pendingListeners.iterator();
              iter.hasNext();)
         {
            NotificationListenerArgs args = iter.next();
            if (safeEquals(listener, args.listener))
            {
               found = true;
               iter.remove();
            }
         }

         if (plainCacheWrapper != null)
         {
            plainCacheWrapper.removeNotificationListener(listener);
         }
         else if (!found)
         {
            throw new ListenerNotFoundException();
         }

      }
   }

   // ---------------------------------------------------------  Public methods

   public MBeanServer getMBeanServer()
   {
      return server;
   }

   /**
    * Sets the configuration that the underlying cache should use.
    *
    * @param config the configuration
    */
   public void setConfiguration(Configuration config)
   {
      this.config = config;
   }

   public void setPojoCache(PojoCache cache)
   {
      if (cacheStatus != CacheStatus.INSTANTIATED
              && cacheStatus != CacheStatus.DESTROYED)
         throw new IllegalStateException("Cannot set underlying cache after call to create()");

      this.pojoCache = cache;
      if (pojoCache == null)
      {
         this.config = null;
         this.plainCacheWrapper = null;
      }
      else
      {
         this.config = cache.getCache().getConfiguration();
         this.plainCacheWrapper = buildPlainCacheWrapper(pojoCache);
      }
   }

   // ---------------------------------------------------------------  Private methods

   private void constructCache() throws ConfigurationException
   {
      pojoCache = (PojoCacheImpl) PojoCacheFactory.createCache(config, false);

      plainCacheWrapper = buildPlainCacheWrapper(pojoCache);
      if (multiplexerService != null)
      {
         injectMuxChannel();
      }
   }

   private CacheJmxWrapper buildPlainCacheWrapper(PojoCache pojoCache)
   {
      CacheJmxWrapper plainCache = new CacheJmxWrapper();
      plainCache.setRegisterInterceptors(getRegisterInterceptors());
      plainCache.setCache(pojoCache.getCache());
      // It shouldn't send out lifecycle state change notifications for itself; 
      // we do it
      plainCache.setDisableStateChangeNotifications(true);

      if (server != null)
      {
         plainCache.setNotificationServiceName(cacheObjectName);
      }

      // Add any NotificationListeners we registered before creating
      // the CacheJmxWrapper
      synchronized (pendingListeners)
      {
         for (NotificationListenerArgs args : pendingListeners)
         {
            plainCache.addNotificationListener(args.listener, args.filter, args.handback);
         }
      }
      return plainCache;
   }

   private boolean registerPlainCache() throws CacheException
   {
      if (registerPlainCache && !plainCacheRegistered && server != null)
      {
         try
         {
            ObjectName ourName = new ObjectName(cacheObjectName);
            ObjectName plainName = JmxUtil.getPlainCacheObjectName(ourName);
            log.debug("Registering plain cache under name " + plainName.getCanonicalName());
            org.jboss.cache.jmx.JmxUtil.registerCacheMBean(server, plainCacheWrapper, plainName.getCanonicalName());
            plainCacheRegistered = true;
            return true;
         }
         catch (JMException e)
         {
            throw new CacheException("Failed to register plain cache", e);
         }
      }

      return false;
   }

   private void unregisterPlainCache()
   {
      if (registerPlainCache && plainCacheRegistered && server != null)
      {
         log.debug("Unregistering plain cache");
         try
         {
            org.jboss.cache.jmx.JmxUtil.unregisterCacheMBean(server, plainCacheWrapper.getCacheObjectName());
         }
         catch (Exception e)
         {
            log.error("Could not unregister plain cache", e);
         }
         plainCacheRegistered = false;
      }
   }

   private void injectMuxChannel() throws CacheException
   {
      Configuration cfg = getConfiguration();
      RuntimeConfig rtcfg = cfg.getRuntimeConfig();

      // Only inject if there isn't already a channel or factory
      if (rtcfg.getMuxChannelFactory() != null && rtcfg.getChannel() != null)
      {
         Channel ch;
         try
         {
            ch = multiplexerService.createMultiplexerChannel(cfg.getMultiplexerStack(), cfg.getClusterName());
         }
         catch (Exception e)
         {
            throw new CacheException("Exception creating multiplexed channel", e);
         }
         rtcfg.setChannel(ch);
      }

   }

   /**
    * Helper for sending out state change notifications
    */
   private void sendStateChangeNotification(int oldState, int newState, String msg, Throwable t)
   {
      long now = System.currentTimeMillis();

      AttributeChangeNotification stateChangeNotification = new AttributeChangeNotification(
              this,
              plainCacheWrapper.getNextNotificationSequenceNumber(), now, msg,
              "State", "java.lang.Integer",
              new Integer(oldState), new Integer(newState)
      );
      stateChangeNotification.setUserData(t);

      plainCacheWrapper.sendNotification(stateChangeNotification);
   }

   /**
    * Sets the cacheStatus to FAILED and rethrows the problem as one
    * of the declared types. Converts any non-RuntimeException Exception
    * to CacheException.
    *
    * @param t
    * @throws PojoCacheException
    * @throws RuntimeException
    * @throws Error
    */
   private void handleLifecycleTransitionFailure(Throwable t)
           throws PojoCacheException, RuntimeException, Error
   {
      int oldState = getState();
      cacheStatus = CacheStatus.FAILED;
      sendStateChangeNotification(oldState, getState(), getClass().getSimpleName() + " failed", t);

      if (t instanceof PojoCacheException)
         throw (PojoCacheException) t;
      if (t instanceof CacheException)
         throw (CacheException) t;
      else if (t instanceof RuntimeException)
         throw (RuntimeException) t;
      else if (t instanceof Error)
         throw (Error) t;
      else
         throw new PojoCacheException(t);
   }

   private static boolean safeEquals(Object us, Object them)
   {
      return (us == null ? them == null : us.equals(them));
   }

   private static class NotificationListenerArgs
   {
      NotificationListener listener;
      NotificationFilter filter;
      Object handback;

      NotificationListenerArgs(NotificationListener listener,
                               NotificationFilter filter,
                               Object handback)
      {
         this.listener = listener;
         this.filter = filter;
         this.handback = handback;
      }

      @Override
      public boolean equals(Object obj)
      {
         if (this == obj) return true;

         if (obj instanceof NotificationListenerArgs)
         {
            NotificationListenerArgs other = (NotificationListenerArgs) obj;
            if (safeEquals(listener, other.listener)
                    && safeEquals(filter, other.filter)
                    && safeEquals(handback, other.handback))
            {
               return true;
            }
         }
         return false;
      }

      @Override
      public int hashCode()
      {
         int result = 17;
         result = 29 * result + (listener != null ? listener.hashCode() : 0);
         result = 29 * result + (filter != null ? filter.hashCode() : 0);
         result = 29 * result + (handback != null ? handback.hashCode() : 0);
         return result;
      }
   }

}
