/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package javax.management.relation;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.management.Attribute;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanException;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.ReflectionException;

import org.jboss.logging.Logger;
import org.jboss.mx.server.MBeanServerImpl;

/**
 * Implements the management interface for a relation service.<p>
 *
 * <p><b>Revisions:</b>
 * <p><b>20020311 Adrian Brock:</b>
 * <ul>
 * <li>Fixed setRole for external MBean and exception handling
 * <li>EmptyStack exception in purging
 * <li>Unregistered mbeans should only contain relation mbeans
 * <li>Unregister notifications not working after change to MBean Filter
 * </ul>
 * <p><b>20020312 Adrian Brock:</b>
 * <ul>
 * <li>Fixed wrong exception types thrown and missing exceptions
 * <li>Allow null role list in createRelation
 * </ul>
 *
 * @see RelationServiceMBean
 *
 * @author  <a href="mailto:Adrian.Brock@HappeningTimes.com">Adrian Brock</a>.
 * @version $Revision: 1.12.4.2 $
 */
public class RelationService
   extends NotificationBroadcasterSupport
   implements RelationServiceMBean, MBeanRegistration, NotificationListener
{
   // Constants -----------------------------------------------------

   private static final long serialVersionUID = 5434016005679159613L;

   private static final Logger log = Logger.getLogger(RelationService.class);

   // Attributes ----------------------------------------------------

   /**
    * Relation ids by relation
    * Note: A relation is an ObjectName for external relations
    * and a RelationSupport object for internal relations
    */
   private HashMap idsByRelation = new HashMap();

   /**
    * The relation service object name
    */
   private ObjectName relationService;

   /**
    * The notification sequence
    */
   private long notificationSequence = 0;

   /**
    * The purge flag
    */
   private boolean purgeFlag;

   /**
    * Relations by relation id
    * Note: A relation is an ObjectName for external relations
    * and a RelationSupport object for internal relations
    */
   private HashMap relationsById = new HashMap();

   /**
    * The mbean server we are registered with
    */
   private MBeanServer server;

   /**
    * The relation types by name
    */
   private HashMap typesByName = new HashMap();

   /**
    * Relation type names by relation ids
    */
   private HashMap typeNamesById = new HashMap();

   /**
    * A notification listener for unregistration
    */
   private MBeanServerNotificationFilter filter;

   /**
    * A list of MBeans unregistered but not yet removed.
    */
   private Stack unregistered = new Stack();

   /**
    * Relation ids an MBean is part of by MBean object name
    * The values side is a HashMap keyed by relation ids
    * with values of a HashSet of role names.
    */
   private HashMap idRolesMapByMBean = new HashMap();

   // The name of the delegate
   private ObjectName delegate;

   // Static --------------------------------------------------------

   // Constructors --------------------------------------------------

   /**
    * Construct a new relation service
    *
    * @param purgeFlag whether immediate purges should be performed,
    *        pass true for immediate, false otherwise
    */
   public RelationService(boolean purgeFlag)
   {
      setPurgeFlag(purgeFlag);
   }

   // RelationServiceMBean implementation ---------------------------

   public synchronized void addRelation(ObjectName relation)
      throws IllegalArgumentException, NoSuchMethodException,
      RelationServiceNotRegisteredException, InvalidRelationIdException,
      InvalidRelationServiceException, RelationTypeNotFoundException,
      InvalidRoleValueException, RoleNotFoundException,
      InstanceNotFoundException
   {
      // Check we have a relation
      if (relation == null)
         throw new IllegalArgumentException("null relation");
      isActive();

      // Get the information we need from the relation
      ObjectName otherService = null;
      String relationId = null;
      String relationTypeName = null;
      RoleList roleList = null;
      try
      {
         server.isInstanceOf(relation, Relation.class.getName());
         otherService = (ObjectName) server.getAttribute(relation,
            "RelationServiceName");
         relationId = (String) server.getAttribute(relation, "RelationId");
         relationTypeName = (String) server.getAttribute(relation,
            "RelationTypeName");
         roleList = (RoleList) server.invoke(relation, "retrieveAllRoles",
            new Object[0], new String[0]);
      }
      catch (InstanceNotFoundException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw new NoSuchMethodException("Not a relation or not registered");
      }
      if (relationId == null)
         throw new InvalidRelationIdException("Null relation id");

      // Check we are in the correct relation service
      if (otherService == null || otherService.equals(relationService) == false)
         throw new InvalidRelationServiceException(otherService + " != " +
            relationService);

      // Create a copy of the role list
      RoleList copy = new RoleList(roleList);

      // Create any missing roles
      createMissingRoles(relationTypeName, copy);

      // Validate the role list
      RoleValidator.validateRoles(relationService, server, relationTypeName,
         copy, false);

      // Add the relation if it is not already present
      validateAndAddRelation(relationId, relation, relationTypeName);

      // Monitor this relation
      filter.enableObjectName(relation);
   }

   public synchronized void addRelationType(RelationType relationType)
      throws IllegalArgumentException, InvalidRelationTypeException
   {
      if (relationType == null)
         throw new IllegalArgumentException("null relation type");
      synchronized (typesByName)
      {
         String name = relationType.getRelationTypeName();
         if (name == null)
            throw new IllegalArgumentException("Null relation type name in relation type");
         if (typesByName.containsKey(name))
            throw new InvalidRelationTypeException("duplicate relation id: " + name);
         validateRelationType(relationType);
         typesByName.put(name, relationType);
      }
   }

   public Integer checkRoleReading(String roleName, String relationTypeName)
      throws IllegalArgumentException, RelationTypeNotFoundException
   {
      if (roleName == null)
         throw new IllegalArgumentException("Null role name");

      // Get the relation type
      RelationType relationType = retrieveRelationTypeForName(relationTypeName);

      // Get the role information
      RoleInfo roleInfo = null;
      try
      {
         roleInfo = relationType.getRoleInfo(roleName);
      }
      catch (RoleInfoNotFoundException e)
      {
         return new Integer(RoleStatus.NO_ROLE_WITH_NAME);
      }

      // Is it readable?
      if (roleInfo.isReadable() == false)
         return new Integer(RoleStatus.ROLE_NOT_READABLE);

      // Yes it is
      return new Integer(0);
   }

   public Integer checkRoleWriting(Role role, String relationTypeName,
      Boolean initFlag)
      throws IllegalArgumentException, RelationTypeNotFoundException
   {
      if (role == null)
         throw new IllegalArgumentException("Null role name");
      if (initFlag == null)
         throw new IllegalArgumentException("Null init flag");

      // Get the relation type
      RelationType relationType = retrieveRelationTypeForName(relationTypeName);

      // Get the role information
      RoleInfo roleInfo = null;
      try
      {
         roleInfo = relationType.getRoleInfo(role.getRoleName());
      }
      catch (RoleInfoNotFoundException e)
      {
         return new Integer(RoleStatus.NO_ROLE_WITH_NAME);
      }

      // Is it writable?
      if (initFlag.booleanValue() == false && roleInfo.isWritable() == false)
         return new Integer(RoleStatus.ROLE_NOT_WRITABLE);

      // Check the cardinality of the role
      ArrayList mbeans = (ArrayList) role.getRoleValue();
      int beanCount = mbeans.size();
      int minimum = roleInfo.getMinDegree();
      if (minimum != RoleInfo.ROLE_CARDINALITY_INFINITY && minimum > beanCount)
        return new Integer(RoleStatus.LESS_THAN_MIN_ROLE_DEGREE);
      int maximum = roleInfo.getMaxDegree();
      if (maximum != RoleInfo.ROLE_CARDINALITY_INFINITY && maximum < beanCount)
        return new Integer(RoleStatus.MORE_THAN_MAX_ROLE_DEGREE);

      // Check the MBeans
      String className = roleInfo.getRefMBeanClassName();
      Iterator iterator = mbeans.iterator();
      while (iterator.hasNext())
      {
        try
        {
          ObjectName objectName = (ObjectName) iterator.next();
          if (server.isInstanceOf(objectName, className) == false)
            return new Integer(RoleStatus.REF_MBEAN_OF_INCORRECT_CLASS);
        }
        catch (Exception e)
        {
          return new Integer(RoleStatus.REF_MBEAN_NOT_REGISTERED);
        }
      }
   
   // Yes it is
      return new Integer(0);
   }

   public synchronized void createRelation(String relationId,
      String relationTypeName, RoleList roleList)
      throws IllegalArgumentException,
      RelationServiceNotRegisteredException, InvalidRelationIdException,
      RelationTypeNotFoundException, InvalidRoleValueException,
      RoleNotFoundException
   {
      // Take a copy of the role list
      RoleList copy = null;
      if (roleList != null)
         copy = new RoleList(roleList);
      else
         copy = new RoleList();

      // Create a relation
      isActive();
      RelationSupport relation = new RelationSupport(relationId,
         relationService,
         server,
         relationTypeName,
         copy);

      // Create any missing roles
      createMissingRoles(relationTypeName, copy);

      // Validate the role list
      RoleValidator.validateRoles(relationService, server, relationTypeName,
         copy, false);

      // Add the relation if it is not already present
      validateAndAddRelation(relationId, relation, relationTypeName);
   }

   public synchronized void createRelationType(String relationTypeName,
      RoleInfo[] roleInfos)
      throws IllegalArgumentException, InvalidRelationTypeException
   {
      if (relationTypeName == null)
         throw new IllegalArgumentException("null relation type name");
      synchronized (typesByName)
      {
         if (typesByName.containsKey(relationTypeName))
            throw new InvalidRelationTypeException("duplicate relation id: "
               + relationTypeName);
         RelationType relationType = new RelationTypeSupport(relationTypeName,
            roleInfos);
         typesByName.put(relationTypeName, relationType);
      }
   }

   public Map findAssociatedMBeans(ObjectName mbeanName,
      String relationTypeName, String roleName)
      throws IllegalArgumentException
   {
      HashMap referencing = (HashMap) findReferencingRelations(mbeanName,
         relationTypeName,
         roleName);
      HashMap result = new HashMap();

      // Loop through our relations
      Iterator relationIterator = referencing.entrySet().iterator();
      while (relationIterator.hasNext())
      {
         Map.Entry referencingEntry = (Map.Entry) relationIterator.next();
         String relationId = (String) referencingEntry.getKey();

         // Get the all beans in this relation
         HashMap referenced = null;
         try
         {
            referenced = (HashMap) getReferencedMBeans(relationId);
         }
         catch (RelationNotFoundException e)
         {
            throw new RuntimeException(e.toString());
         }

         // Check each bean's roles
         Iterator mbeanIterator = referenced.entrySet().iterator();
         while (mbeanIterator.hasNext())
         {

            Map.Entry referencedEntry = (Map.Entry) mbeanIterator.next();
            ObjectName objectName = (ObjectName) referencedEntry.getKey();

            // Exclude ourselves from the test
            if (objectName.equals(mbeanName) == false)
            {
               // Ok this is one of our associated mbeans
               ArrayList resultList = (ArrayList) result.get(objectName);
               if (resultList == null)
               {
                  resultList = new ArrayList();
                  resultList.add(relationId);
                  result.put(objectName, resultList);
               }
               else if (resultList.contains(relationId) == false)
                  resultList.add(relationId);
            }
         }
      }
      // All done
      return result;
   }

   public Map findReferencingRelations(ObjectName mbeanName,
      String relationTypeName, String roleName)
      throws IllegalArgumentException
   {
      if (mbeanName == null)
         throw new IllegalArgumentException("null object name");

      HashMap result = new HashMap();

      // Get the relations to roles map for the passed mbean
      HashMap idRolesMap = (HashMap) idRolesMapByMBean.get(mbeanName);
      if (idRolesMap == null)
         return result;
      Iterator iterator = idRolesMap.entrySet().iterator();
      while (iterator.hasNext())
      {
         Map.Entry entry = (Map.Entry) iterator.next();
         String relationId = (String) entry.getKey();
         HashSet roleNames = (HashSet) entry.getValue();

         // See if we have the correct relation type
         if (relationTypeName == null ||
            typeNamesById.get(relationId).equals(relationTypeName))
         {
            ArrayList resultRoleNames = new ArrayList();
  
            // No role specified, add them all
            if (roleName == null)
               resultRoleNames.addAll(roleNames);
            // See if we have this role
            else if (roleNames.contains(roleName) &&
               resultRoleNames.contains(roleName) == false)
               resultRoleNames.add(roleName);

            // Did we find anything, use it
            if (resultRoleNames.size() > 0)
               result.put(relationId, resultRoleNames);
         }
      }
      // All done
      return result;
   }

   public List findRelationsOfType(String relationTypeName)
      throws IllegalArgumentException, RelationTypeNotFoundException
   {
      if (relationTypeName == null)
         throw new IllegalArgumentException("null relation type name");
      if (typesByName.containsKey(relationTypeName) == false)
         throw new RelationTypeNotFoundException("relation type name not found");

      // Build the list
      ArrayList result = new ArrayList();
      Iterator iterator = typeNamesById.entrySet().iterator();
      while (iterator.hasNext())
      {
         Map.Entry entry = (Map.Entry) iterator.next();
         String typeName = (String) entry.getValue();
         if (typeName.equals(relationTypeName))
            result.add(entry.getKey());
      }
      // All done
      return result;
   }

   public List getAllRelationIds()
   {
      ArrayList result = new ArrayList(relationsById.size());
      synchronized (relationsById)
      {
         Iterator iterator = relationsById.keySet().iterator();
         while (iterator.hasNext())
            result.add(iterator.next());
      }
      return result;
   }

   public List getAllRelationTypeNames()
   {
      ArrayList result = new ArrayList(typesByName.size());
      synchronized (typesByName)
      {
         Iterator iterator = typesByName.keySet().iterator();
         while (iterator.hasNext())
            result.add(iterator.next());
      }
      return result;
   }

   public RoleResult getAllRoles(String relationId)
      throws IllegalArgumentException, RelationNotFoundException,
      RelationServiceNotRegisteredException
   {
      isActive();
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation for the roles
      if (relation instanceof RelationSupport)
      {
         return ((RelationSupport) relation).getAllRoles();
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            return (RoleResult) server.getAttribute(objectName, "AllRoles");
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (Exception e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public boolean getPurgeFlag()
   {
      return purgeFlag;
   }

   public Map getReferencedMBeans(String relationId)
      throws IllegalArgumentException, RelationNotFoundException
   {
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation for the referenced mbeans
      if (relation instanceof RelationSupport)
      {
         return ((RelationSupport) relation).getReferencedMBeans();
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            return (Map) server.getAttribute(objectName, "ReferencedMBeans");
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (Exception e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public String getRelationTypeName(String relationId)
      throws IllegalArgumentException, RelationNotFoundException
   {
      return retrieveTypeNameForId(relationId);
   }

   public List getRole(String relationId, String roleName)
      throws IllegalArgumentException, RelationNotFoundException,
      RelationServiceNotRegisteredException, RoleNotFoundException
   {
      // Get the relation object name
      if (roleName == null)
         throw new IllegalArgumentException("null role");
      isActive();
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation for the role value
      if (relation instanceof RelationSupport)
      {
         return ((RelationSupport) relation).getRole(roleName);
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            List result = (List) server.invoke(objectName, "getRole",
               new Object[]{roleName},
               new String[]{"java.lang.String"});
            return result;
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (MBeanException mbe)
         {
            Exception e = mbe.getTargetException();
            if (e instanceof RoleNotFoundException)
               throw (RoleNotFoundException) e;
            else
               throw new RuntimeException(e.toString());
         }
         catch (ReflectionException e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public Integer getRoleCardinality(String relationId, String roleName)
      throws IllegalArgumentException, RelationNotFoundException,
      RoleNotFoundException
   {
      // Get the relation object name
      if (roleName == null)
         throw new IllegalArgumentException("null role");
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation for the role cardinality
      if (relation instanceof RelationSupport)
      {
         return ((RelationSupport) relation).getRoleCardinality(roleName);
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            Integer result = (Integer) server.invoke(
               objectName, "getRoleCardinality",
               new Object[]{roleName},
               new String[]{"java.lang.String"});
            return result;
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (MBeanException mbe)
         {
            Exception e = mbe.getTargetException();
            if (e instanceof RoleNotFoundException)
               throw (RoleNotFoundException) e;
            else
               throw new RuntimeException(e.toString());
         }
         catch (ReflectionException e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public RoleInfo getRoleInfo(String relationTypeName, String roleInfoName)
      throws IllegalArgumentException, RelationTypeNotFoundException,
      RoleInfoNotFoundException
   {
      // Get the relation type
      RelationType relationType = retrieveRelationTypeForName(relationTypeName);

      // Return the role information
      return relationType.getRoleInfo(roleInfoName);
   }

   public List getRoleInfos(String relationTypeName)
      throws IllegalArgumentException, RelationTypeNotFoundException
   {
      // Get the relation type
      RelationType relationType = retrieveRelationTypeForName(relationTypeName);

      // Return the role information
      return relationType.getRoleInfos();
   }

   public RoleResult getRoles(String relationId, String[] roleNames)
      throws IllegalArgumentException, RelationNotFoundException,
      RelationServiceNotRegisteredException
   {
      // Get the relation object name
      if (roleNames == null)
         throw new IllegalArgumentException("null role names");
      isActive();
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation for the role value
      if (relation instanceof RelationSupport)
      {
         return ((RelationSupport) relation).getRoles(roleNames);
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            RoleResult result = (RoleResult) server.invoke(objectName, "getRoles",
               new Object[]{roleNames},
               new String[]{new String[0].getClass().getName()});
            return result;
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (MBeanException e)
         {
            throw new RuntimeException(e.toString());
         }
         catch (ReflectionException e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public Boolean hasRelation(String relationId)
      throws IllegalArgumentException
   {
      if (relationId == null)
         throw new IllegalArgumentException("null relation id");
      return new Boolean(relationsById.get(relationId) != null);
   }

   public void isActive()
      throws RelationServiceNotRegisteredException
   {
      if (server == null)
         throw new RelationServiceNotRegisteredException("Not registered");
   }

   public String isRelation(ObjectName objectName)
      throws IllegalArgumentException
   {
      if (objectName == null)
         throw new IllegalArgumentException("null object name");
      return (String) idsByRelation.get(objectName);
   }

   public ObjectName isRelationMBean(String relationId)
      throws IllegalArgumentException, RelationNotFoundException
   {
      if (relationId == null)
         throw new IllegalArgumentException("null relation id");
      Object result = relationsById.get(relationId);
      if (result == null)
         throw new RelationNotFoundException(relationId);
      if (result instanceof ObjectName)
         return (ObjectName) result;
      else
         return null;
   }

   public void purgeRelations()
      throws RelationServiceNotRegisteredException
   {
      isActive();
      // Keep going until they are all done
      while (unregistered.empty() == false)
      {
         // Get the next object
         ObjectName mbean = (ObjectName) unregistered.pop();

         // Keep track of the remain relations/roles
         HashMap relationRoles = new HashMap();
      
         // Get the relations for this mbean
         HashMap idRolesMap = (HashMap) idRolesMapByMBean.get(mbean);

         // Go through each relation/role
         Iterator iterator = idRolesMap.entrySet().iterator();
         while (iterator.hasNext())
         {
            Map.Entry entry = (Map.Entry) iterator.next();
            String relationId = (String) entry.getKey();
            HashSet roleNames = (HashSet) entry.getValue();
            Iterator inner = roleNames.iterator();
            while (inner.hasNext())
            {
               String roleName = (String) inner.next();
               relationRoles.put(relationId, roleName);
            }
         }

         // Tell the relation about the removed role
         iterator = relationRoles.entrySet().iterator();
         while (iterator.hasNext())
         {
            Map.Entry entry = (Map.Entry) iterator.next();
            String relationId = (String) entry.getKey();
            String roleName = (String) entry.getValue();
            Object relation = relationsById.get(relationId);
            String typeName = (String) typeNamesById.get(relationId);
            RelationType relationType = (RelationType) typesByName.get(typeName);
            if (relation instanceof RelationSupport)
            {
               RelationSupport support = (RelationSupport) relation;
               try
               {
                  // Check to see if the relation is now invalid
                  Integer cardinality = support.getRoleCardinality(roleName);
                  RoleInfo roleInfo = relationType.getRoleInfo(roleName);
                  int minDegree = roleInfo.getMinDegree();

                  // It's invalid, remove it
                  if (cardinality.intValue() == minDegree)
                     removeRelation(relationId);
                  else
                  // It's ok tell the relation about the unregistration
                     support.handleMBeanUnregistration(mbean, roleName);
               }
               catch (Exception e)
               {
                  log.debug("Error during purge", e);
               }
            }
            else
            {
               try
               {
                  // Check to see if the relation is now invalid
                  ObjectName objectName = (ObjectName) relation;
                  Integer cardinality = (Integer) server.invoke(
                     objectName, "getRoleCardinality",
                     new Object[]{roleName},
                     new String[]{"java.lang.String"});
                  RoleInfo roleInfo = relationType.getRoleInfo(roleName);
                  int minDegree = roleInfo.getMinDegree();

                  // It's invalid, remove it
                  if (cardinality.intValue() == minDegree)
                     removeRelation(relationId);
                  else
                  // It's ok tell the relation about the unregistration
                     server.invoke(objectName, "handleMBeanUnregistration",
                        new Object[]{mbean, roleName},
                        new String[]{"java.lang.String", "java.lang.String "});
               }
               catch (MBeanException mbe)
               {
                  log.debug("Error during purge", mbe.getTargetException());
               }
               catch (Exception e)
               {
                  log.debug("Error during purge", e);
               }
            }
         }
      }
   }

   public synchronized void removeRelation(String relationId)
      throws IllegalArgumentException, RelationNotFoundException,
      RelationServiceNotRegisteredException
   {
      isActive();

      // Get the MBeans that are part of this relation
      ArrayList unregMBeans = new ArrayList(
         getReferencedMBeans(relationId).keySet());

      // Check to see whether this will remove the MBean
      Iterator iterator = unregMBeans.iterator();
      while (iterator.hasNext())
      {
         // Remove the MBeans relation role map
         ObjectName mbean = (ObjectName) iterator.next();
         HashMap idRolesMap = (HashMap) idRolesMapByMBean.get(mbean);
         idRolesMap.remove(relationId);

         // We were the last?
         if (idRolesMap.size() == 0)
            idRolesMapByMBean.remove(mbean);

         // Is this an MBean a relation?
         if (idsByRelation.containsKey(mbean) == false)
            iterator.remove();
      }

      // Send a notification of the removal
      sendRelationRemovalNotification(relationId, unregMBeans);

      // Remove the relation from all the data structures
      Object relation = retrieveRelationForId(relationId);
      relationsById.remove(relationId);
      idsByRelation.remove(relation);
      typeNamesById.remove(relationId);

      // Update the relation management flag
      if (relation instanceof ObjectName)
      {
         try
         {
            Attribute attribute = new Attribute("RelationServiceManagementFlag",
               new Boolean(false));
            server.setAttribute((ObjectName) relation, attribute);
         }
         catch (Exception doesntImplementRelationSupportMBean)
         {
         }

         // Don't monitor this relation anymore
         filter.disableObjectName((ObjectName) relation);
      }
   }

   public synchronized void removeRelationType(String relationTypeName)
      throws IllegalArgumentException, RelationTypeNotFoundException,
      RelationServiceNotRegisteredException
   {
      if (relationTypeName == null)
         throw new IllegalArgumentException("null relation type name");
      isActive();
      if (typesByName.containsKey(relationTypeName) == false)
         throw new RelationTypeNotFoundException("relation type name not found");

      // Remove any relations for this relation type
      // FIXME: This is rubbish
      ArrayList ids = new ArrayList();
      Map.Entry entry;
      Iterator iterator = typeNamesById.entrySet().iterator();
      while (iterator.hasNext())
      {
         entry = (Map.Entry) iterator.next();
         if (entry.getValue().equals(relationTypeName))
            ids.add(entry.getKey());
      }
      iterator = ids.iterator();
      while (iterator.hasNext())
      {
         try
         {
            removeRelation((String) iterator.next());
         }
         catch (RelationNotFoundException ignored)
         {
         }
      }

      // Remove the relation type
      typesByName.remove(relationTypeName);
   }

   public void sendRelationCreationNotification(String relationId)
      throws IllegalArgumentException, RelationNotFoundException
   {
      // Determine the type of relation
      String type = null;
      String description = null;
      if (relationsById.get(relationId) instanceof RelationSupport)
      {
         type = RelationNotification.RELATION_BASIC_CREATION;
         description = "Creation of internal relation.";
      }
      else
      {
         type = RelationNotification.RELATION_MBEAN_CREATION;
         description = "Creation of external relation.";
      }
      // Send the notification
      sendNotification(type, description, relationId, null, null, null);
   }

   public void sendRelationRemovalNotification(String relationId,
      List unregMBeans)
      throws IllegalArgumentException, RelationNotFoundException
   {
      // Determine the type of relation
      String type = null;
      String description = null;
      if (relationsById.get(relationId) instanceof RelationSupport)
      {
         type = RelationNotification.RELATION_BASIC_REMOVAL;
         description = "Removal of internal relation.";
      }
      else
      {
         type = RelationNotification.RELATION_MBEAN_REMOVAL;
         description = "Removal of external relation.";
      }
      // Send the notification
      sendNotification(type, description, relationId, unregMBeans, null, null);
   }

   public void sendRoleUpdateNotification(String relationId,
      Role newRole, List oldRoleValue)
      throws IllegalArgumentException, RelationNotFoundException
   {
      // Determine the type of relation
      String type = null;
      String description = null;
      if (relationsById.get(relationId) instanceof RelationSupport)
      {
         type = RelationNotification.RELATION_BASIC_UPDATE;
         description = "Update of internal relation.";
      }
      else
      {
         type = RelationNotification.RELATION_MBEAN_UPDATE;
         description = "Update of external relation.";
      }
      if (newRole == null)
         throw new IllegalArgumentException("null role");

      if (oldRoleValue == null)
         throw new IllegalArgumentException("null old role value");

      // Send the notification
      sendNotification(type, description, relationId, null, newRole, oldRoleValue);
   }

   public void setPurgeFlag(boolean value)
   {
      purgeFlag = value;
   }

   public void setRole(String relationId, Role role)
      throws RelationServiceNotRegisteredException,
      IllegalArgumentException,
      RelationNotFoundException,
      RoleNotFoundException,
      InvalidRoleValueException
   {
      // Get the relation object name
      if (role == null)
         throw new IllegalArgumentException("null role");
      isActive();
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation to set the role
      if (relation instanceof RelationSupport)
      {
         try
         {
            ((RelationSupport) relation).setRole(role);
         }
         catch(RelationTypeNotFoundException e)
         {
            RelationNotFoundException rnfe = new RelationNotFoundException(e.getMessage());
            rnfe.initCause(e);
            throw rnfe;
         }
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            server.setAttribute(objectName, new Attribute("Role", role));
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (MBeanException mbe)
         {
            Exception e = mbe.getTargetException();
            if (e instanceof RoleNotFoundException)
               throw (RoleNotFoundException) e;
            else if (e instanceof InvalidRoleValueException)
               throw (InvalidRoleValueException) e;
            else
               throw new RuntimeException(e.toString());
         }
         catch (AttributeNotFoundException e)
         {
            throw new RuntimeException(e.toString());
         }
         catch (InvalidAttributeValueException e)
         {
            throw new RuntimeException(e.toString());
         }
         catch (ReflectionException e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public RoleResult setRoles(String relationId, RoleList roles)
      throws IllegalArgumentException, RelationServiceNotRegisteredException,
      RelationNotFoundException
   {
      // Get the relation object name
      if (roles == null)
         throw new IllegalArgumentException("null roles");
      isActive();
      Object relation = retrieveRelationForId(relationId);
    
      // Ask the relation to set the roles
      if (relation instanceof RelationSupport)
      {
         try
         {
            return ((RelationSupport) relation).setRoles(roles);
         }
            // Why doesn't it throw this?
         catch (RelationTypeNotFoundException e)
         {
            throw new RuntimeException(e.toString());
         }
      }
      else
      {
         ObjectName objectName = (ObjectName) relation;
         try
         {
            RoleResult result = (RoleResult) server.invoke(objectName, "setRoles",
               new Object[]{roles},
               new String[]{"javax.management.relation.RoleList"});
            return result;
         }
         catch (InstanceNotFoundException e)
         {
            throw new RelationNotFoundException(objectName.toString());
         }
         catch (MBeanException e)
         {
            throw new RuntimeException(e.getTargetException().toString());
         }
         catch (ReflectionException e)
         {
            throw new RuntimeException(e.toString());
         }
      }
   }

   public void updateRoleMap(String relationId, Role newRole,
      List oldRoleValue)
      throws IllegalArgumentException, RelationServiceNotRegisteredException,
      RelationNotFoundException
   {
      if (relationId == null)
         throw new IllegalArgumentException("null relation id");
      if (newRole == null)
         throw new IllegalArgumentException("null role");
      if (oldRoleValue == null)
         throw new IllegalArgumentException("null old role value");
      isActive();

      if (relationsById.containsKey(relationId) == false)
         throw new RelationNotFoundException("Invalid relation id: " + relationId);

      // Get the role name and new Value
      String roleName = newRole.getRoleName();
      ArrayList newRoleValue = (ArrayList) newRole.getRoleValue();

      // Remove the unused and unchanged object names
      Iterator iterator = oldRoleValue.iterator();
      while (iterator.hasNext())
      {
         ObjectName objectName = (ObjectName) iterator.next();
         if (newRoleValue.contains(objectName) == false)
         {
            // We have to remove this relation/role
            HashMap idRolesMap = (HashMap) idRolesMapByMBean.get(objectName);
            HashSet roleNames = (HashSet) idRolesMap.get(relationId);
            roleNames.remove(roleName);
            if (roleNames.size() == 0)
            {
               idRolesMap.remove(relationId);
               if (idRolesMap.size() == 0)
               {
                  idRolesMapByMBean.remove(objectName);
                  filter.disableObjectName(objectName);
               }
            }
         }
      }

      // Make sure all the roles exist
      iterator = newRoleValue.iterator();
      while (iterator.hasNext())
      {
         ObjectName objectName = (ObjectName) iterator.next();
         HashMap idRolesMap = (HashMap) idRolesMapByMBean.get(objectName);
         if (idRolesMap == null)
         {
            idRolesMap = new HashMap();
            idRolesMapByMBean.put(objectName, idRolesMap);
            filter.enableObjectName(objectName);
         }
         HashSet roleNames = (HashSet) idRolesMap.get(relationId);
         if (roleNames == null)
         {
            roleNames = new HashSet();
            idRolesMap.put(relationId, roleNames);
         }
         if (roleNames.contains(roleName) == false)
            roleNames.add(roleName);
      }
   }

   // MBeanRegistration implementation ------------------------------

   public ObjectName preRegister(MBeanServer server, ObjectName objectName)
      throws Exception
   {
      // Save some data
      this.server = server;
      this.relationService = objectName;

      // Install our notification listener we aren't interested in registration
      // We aren't monitoring anything at start-up
      filter = new MBeanServerNotificationFilter();
      filter.enableType(MBeanServerNotification.UNREGISTRATION_NOTIFICATION);
      filter.disableAllObjectNames();
      delegate = new ObjectName(MBeanServerImpl.MBEAN_SERVER_DELEGATE);
      server.addNotificationListener(delegate, this, filter, null);
    
      // Ok go ahead and register
      return objectName;
   }

   public void postRegister(Boolean registered)
   {
   }

   public void preDeregister()
      throws Exception
   {
   }

   public void postDeregister()
   {
      try
      {
         // Remove the notification listener
         server.removeNotificationListener(delegate, this);
      }
      catch (Exception ignored)
      {
      }

      // REVIEW: Should we remove relation types/relations here?

      // We are no longer registered
      server = null;
   }

   // NotificationListener implementation ----------------------------

   public void handleNotification(Notification notification, Object handback)
   {
      // Make sure we are not called malicously
      if (notification == null ||
         !(notification instanceof MBeanServerNotification))
         return;

      // It still might be malicous
      MBeanServerNotification mbsn = (MBeanServerNotification) notification;
      if (mbsn.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION) == false)
         return;
      // Add the object to the list of mbeans to remove
      ObjectName objectName = mbsn.getMBeanName();
      unregistered.push(objectName);

      try
      {
         // Are we set to automatic purge?
         if (purgeFlag == true)
            purgeRelations();
      }
      catch (Exception ignored)
      {
      }

      try
      {
         // Is this a relation?
         String relationId = (String) idsByRelation.get(objectName);
         if (relationId != null)
            removeRelation(relationId);
      }
      catch (Exception ignored)
      {
      }
   }

   // NotificationBroadcasterSupport overrides -----------------------

   public MBeanNotificationInfo[] getNotificationInfo()
   {
      MBeanNotificationInfo[] result = new MBeanNotificationInfo[1];
      String[] types = new String[]
      {
         RelationNotification.RELATION_BASIC_CREATION,
         RelationNotification.RELATION_BASIC_REMOVAL,
         RelationNotification.RELATION_BASIC_UPDATE,
         RelationNotification.RELATION_MBEAN_CREATION,
         RelationNotification.RELATION_MBEAN_REMOVAL,
         RelationNotification.RELATION_MBEAN_UPDATE
      };
      result[0] = new MBeanNotificationInfo(types,
         "javax.management.relation.RelationNotification",
         "Notifications sent by the Relation Service MBean");
      return result;
   }

   // Private --------------------------------------------------------

   /**
    * Create missing roles
    *
    * @param relationTypeName the relation type name
    * @param roleList the existing roles
    * @exception RelationTypeNotFoundException for a missing relation type
    */
   private void createMissingRoles(String relationTypeName, RoleList roleList)
      throws RelationTypeNotFoundException
   {
      if (relationTypeName == null)
         throw new RelationTypeNotFoundException("RelationType name null not found");

      // Get the relation type
      RelationType relationType = retrieveRelationTypeForName(relationTypeName);

      // Get the Role Information
      ArrayList roleInfos = (ArrayList) relationType.getRoleInfos();

      // Add empty roles for missing roles
      Iterator iterator = roleInfos.iterator();
      while (iterator.hasNext())
      {
         RoleInfo roleInfo = (RoleInfo) iterator.next();
         boolean found = false;
         Iterator inner = roleList.iterator();
         while (inner.hasNext())
         {
            Role role = (Role) inner.next();
            if (role.getRoleName().equals(roleInfo.getName()))
            {
               found = true;
               break;
            }
         }
         if (found == false)
            roleList.add(new Role(roleInfo.getName(), new RoleList()));
      }
   }

   /**
    * Get the relation for a relation id
    *
    * @param relationId the relation id
    * @return the relation's object name or a relation support object
    * @exception IllegalArgumentException for a null relation id
    * @exception RelationNotFoundException when the relation is not registered
    */
   private Object retrieveRelationForId(String relationId)
      throws IllegalArgumentException, RelationNotFoundException
   {
      if (relationId == null)
         throw new IllegalArgumentException("null relation id");
      Object result = relationsById.get(relationId);
      if (result == null)
         throw new RelationNotFoundException(relationId);
      return result;
   }

   /**
    * Get the relation type name for a relation id
    *
    * @param relationId the relation id
    * @return the relation type name
    * @exception IllegalArgumentException for a null relation id
    * @exception RelationNotFoundException when the relation is not registered
    */
   private String retrieveTypeNameForId(String relationId)
      throws IllegalArgumentException, RelationNotFoundException
   {
      if (relationId == null)
         throw new IllegalArgumentException("null relation id");
      String result = (String) typeNamesById.get(relationId);
      if (result == null)
         throw new RelationNotFoundException(relationId);
      return result;
   }

   /**
    * Get the relation type for a relation type name
    *
    * @param relationTypeName the relation type name
    * @return the relation type
    * @exception IllegalArgumentException for a null relation type name
    * @exception RelationTypeNotFoundException when the relation is not 
    *            registered.
    */
   private RelationType retrieveRelationTypeForName(String relationTypeName)
      throws IllegalArgumentException, RelationTypeNotFoundException
   {
      if (relationTypeName == null)
         throw new IllegalArgumentException("Null relation type name");
      // Get the relation type
      RelationType result = (RelationType) typesByName.get(relationTypeName);
      if (result == null)
         throw new RelationTypeNotFoundException(relationTypeName);
      return result;
   }

   /**
    * Send a notification.
    *
    * @param type the notification type
    * @param description the human readable description
    * @param relationId the relation id of the relation
    * @param unregMBeans the mbeans removed when a relation is removed
    * @param newRole the role after an update
    * @param oldRoleValue the old role values after an update
    * @exception IllegalArgumentException for a null relation id
    * @exception RelationNotFoundException for an invalid relation id.
    */
   private void sendNotification(String type, String description,
      String relationId, List unregMBeans,
      Role newRole, List oldRoleValue)
      throws IllegalArgumentException, RelationNotFoundException
   {
      if (type == null)
         throw new IllegalArgumentException("null notification type");

      // Get the relation type name
      String typeName = retrieveTypeNameForId(relationId);

      // Get the relation's object name (if it has one)
      Object relation = retrieveRelationForId(relationId);
      ObjectName relationName = null;
      if (relation instanceof ObjectName)
         relationName = (ObjectName) relation;

      // Get the next sequence number
      long sequence;
      synchronized (this)
      {
         sequence = ++notificationSequence;
      }
      // Send the notification
      // REVIEW: According to the spec, the source should be a RelationService
      // that doesn't make any sense, it's not serializable
      // I use the object name
      if (type.equals(RelationNotification.RELATION_BASIC_UPDATE) ||
         type.equals(RelationNotification.RELATION_MBEAN_UPDATE))
         sendNotification(new RelationNotification(type, relationService, sequence,
            System.currentTimeMillis(), description, relationId, typeName,
            relationName, newRole.getRoleName(), newRole.getRoleValue(),
            oldRoleValue));
      else
         sendNotification(new RelationNotification(type, relationService, sequence,
            System.currentTimeMillis(), description, relationId, typeName,
            relationName, unregMBeans));
   }

   /**
    * Validate and add the relation.
    *
    * @param relationId the relation id
    * @param relation the relation to add
    * @param relationTypeName the relation type name
    * @exception InvalidRelationIdException when it is already present
    * @exception RelationNotFoundException when an error occurs invoking the
    *            relation
    * @exception IllegalArgumentException when there is an error in the
    *            parameters
    * @exception RelationServiceNotRegisteredException when the relation
    *            service has not started.
    */
   private synchronized void validateAndAddRelation(String relationId,
      Object relation, String relationTypeName)
      throws InvalidRelationIdException
   {
      if (relationsById.containsKey(relationId))
         throw new InvalidRelationIdException(relationId);
      relationsById.put(relationId, relation);
      idsByRelation.put(relation, relationId);
      typeNamesById.put(relationId, relationTypeName);

      // Retrieve all the roles
      RoleList roles = null;
      if (relation instanceof RelationSupport)
      {
         RelationSupport support = (RelationSupport) relation;
         roles = support.retrieveAllRoles();
      }
      else
      {
         try
         {
            roles = (RoleList) server.invoke((ObjectName) relation,
               "retrieveAllRoles", new Object[0], new String[0]);
         }
         catch (Exception e)
         {
            throw new RuntimeException(e.toString());
         }
      }

      // Update the roles
      Iterator iterator = roles.iterator();
      while (iterator.hasNext())
      {
         Role role = (Role) iterator.next();
         try
         {
            updateRoleMap(relationId, role, role.getRoleValue());
         }
         catch (Exception e)
         {
            throw new RuntimeException(e.toString());
         }
      }

      // We are now managing this relation
      if (relation instanceof RelationSupport)
      {
         RelationSupport support = (RelationSupport) relation;
         support.setRelationServiceManagementFlag(new Boolean(true));
      }
      else
      {
         try
         {
            Attribute attribute = new Attribute("RelationServiceManagementFlag",
               new Boolean(true));
            server.setAttribute((ObjectName) relation, attribute);
         }
         catch (Exception doesntImplementRelationSupportMBean)
         {
         }
      }

      // Send a notification of the addition
      try
      {
         sendRelationCreationNotification(relationId);
      }
      catch (RelationNotFoundException e)
      {
         throw new RuntimeException(e.toString());
      }
   }

   /**
    * Validate a relation type.
    *
    * @param relationType the relation to validate
    * @exception InvalidRelationTypeException when it is not valid
    */
   private void validateRelationType(RelationType relationType)
      throws InvalidRelationTypeException
   {
      // Check the role information
      HashSet roleNames = new HashSet();
      ArrayList roleInfos = (ArrayList) relationType.getRoleInfos();
      if (roleInfos == null)
         throw new InvalidRelationTypeException("Null role infos");
      if (roleInfos.size() == 0)
         throw new InvalidRelationTypeException("No role infos");
      synchronized (roleInfos)
      {
         Iterator iterator = roleInfos.iterator();
         while (iterator.hasNext())
         {
            RoleInfo roleInfo = (RoleInfo) iterator.next();
            if (roleInfo == null)
               throw new InvalidRelationTypeException("Null role");
            if (roleNames.contains(roleInfo.getName()))
               throw new InvalidRelationTypeException(
                  "Duplicate role name" + roleInfo.getName());
            roleNames.add(roleInfo.getName());
         }
      }
   }
}