/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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 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.util.Primitives;
import org.jboss.mx.server.MBeanServerImpl;

/**
 * Implements the management interface for a relation service.<p>
 *
 * @see RelationServiceMBean
 *
 * @author  <a href="mailto:Adrian.Brock@HappeningTimes.com">Adrian Brock</a>.
 * @version $Revision: 1.6.6.6 $
 */
public class RelationService
  extends NotificationBroadcasterSupport
  implements RelationServiceMBean, MBeanRegistration, NotificationListener
{
  // Constants -----------------------------------------------------

  // 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");
    }

    // Check we are in the correct relation service
    if (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 (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);

    // 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();
      ArrayList referencingRoleNames = (ArrayList) referencingEntry.getValue();

      // 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)
        {
          ArrayList referencedRoleNames = (ArrayList) referencedEntry.getValue();

          // Do we share a role?
          Iterator roleIterator = referencedRoleNames.iterator();
          while (roleIterator.hasNext())
          {
            String currentRoleName = (String) roleIterator.next();
            if (referencedRoleNames.contains(currentRoleName))
            {
              // Ok this is one of our associated mbeans
              ArrayList resultList = (ArrayList) result.get(objectName);
              if (resultList == null)
              {
                resultList = new ArrayList();
                resultList.add(currentRoleName);
                result.put(objectName, resultList);
              }
              else if (resultList.contains(currentRoleName) == false)
                resultList.add(currentRoleName);
              // We've findished with this mbean do the next
              break;
            }
          }
        }
      }
    }
    // 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);
    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.add(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((String) 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 Primitives.valueOf(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 = (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)
          {
            throw new RuntimeException(e.toString());
          }
        }
        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 = (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)
          {
            throw new RuntimeException(mbe.getTargetException().toString());
          }
          catch (Exception e)
          {
            throw new RuntimeException(e.toString());
          }
        }
      }      
    }
  }

  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",
                                            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 IllegalArgumentException, RelationServiceNotRegisteredException,
           RelationNotFoundException, RoleNotFoundException,
           InvalidRoleValueException, RelationTypeNotFoundException
  {
    // 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)
    {
      ((RelationSupport) relation).setRole(role);
    }
    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 IllegalArgumentException for a null relation id
   * @exception RelationTypeNotFoundException for a missing relation type
   */
  private void createMissingRoles(String relationTypeName, RoleList roleList)
    throws IllegalArgumentException, RelationTypeNotFoundException
  {
    // 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();
    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());
      }
    }
  }
}
